Files
mermaid-js-mermaid/packages/mermaid/src/diagrams/flowchart/styles.ts
T

299 lines
8.4 KiB
TypeScript

// import khroma from 'khroma';
import * as khroma from 'khroma';
import { getIconStyles } from '../globalStyles.js';
import { colorSlotCount, hasPalette, isColorTheme, safeLook } from '../common/colorThemeGate.js';
/** Returns the styles given options */
export interface FlowChartStyleOptions {
arrowheadColor: string;
border2: string;
clusterBkg: string;
clusterBorder: string;
edgeLabelBackground: string;
fontFamily: string;
lineColor: string;
mainBkg: string;
nodeBorder: string;
nodeTextColor: string;
tertiaryColor: string;
textColor: string;
titleColor: string;
strokeWidth: string;
theme?: string;
look?: string;
THEME_COLOR_LIMIT?: number;
borderColorArray?: string[];
bkgColorArray?: string[];
}
/**
* Cycling per-subgraph colour. Only the containers are painted -- the nodes inside keep
* the uniform look, because a flowchart node is a step in a flow rather than a distinct
* participant, and node colour is already how `classDef` / `style` carry meaning.
*
* Emits both the `rect` (classic/neo) and `path` (handDrawn) forms since the container is
* a plain rect in one look and a roughjs path pair in the other. `.collapsed-group` is
* the same container drawn as a compact node by `collapsedGroup.ts` — it is a container,
* not one of the flow's steps, so it takes the palette too; without it a collapsed
* subgraph rendered uncoloured beside tinted siblings.
*
* Not `!important`: `clusters.js` and `collapsedGroup.ts` both put user styles in an
* inline `style` attribute, which has to keep winning over the theme palette. The
* collapsed form's own colours are presentation attributes (`fill=` / `stroke=`), which
* these rules correctly outrank while still losing to that inline style.
*
* Lanes are excluded by `:not(.swimlane)` and ruled separately below: a lane is two
* rectangles, and the generic `path` rule would paint the hachure roughjs emits for its
* unfilled body. Here, not in `swimlanes/styles.ts`, so `layout: swimlane` is covered too.
*/
const genColor = (options: FlowChartStyleOptions) => {
const { theme, bkgColorArray, borderColorArray } = options;
if (!isColorTheme(theme, borderColorArray)) {
return '';
}
const look = safeLook(options.look);
const hasBkgColors = hasPalette(bkgColorArray);
let sections = '';
for (let i = 0; i < colorSlotCount(options.THEME_COLOR_LIMIT, borderColorArray); i++) {
const borderColor = borderColorArray![i % borderColorArray!.length];
const fill = hasBkgColors ? `fill: ${bkgColorArray[i % bkgColorArray.length]};` : '';
const slot = `[data-look="${look}"][data-color-id="color-${i}"]`;
/* A collapsed subgraph is drawn by `collapsedGroup.ts` through `getNodeClasses`, which
* returns `rough-node` instead of `node` for the handDrawn look -- so a `.node`-only
* selector leaves handDrawn collapsed containers uncoloured beside their tinted
* siblings. Clusters are unaffected: `clusters.js` sets the `cluster` class directly.
*
* Each descendant has to be appended to *both* prefixes separately. Writing
* `${slot}.node, ${slot}.rough-node .thing` would attach the descendant to the last
* item of the list only, silently matching nothing under the classic look.
*/
const collapsedRule = (suffix: string) =>
`${slot}.node ${suffix}, ${slot}.rough-node ${suffix}`;
/* Both halves of a lane. Each carries the whole prefix separately, as above. */
const laneRule = (suffix: string) =>
`${slot}.swimlane.cluster .swimlane-title${suffix}, ${slot}.swimlane.cluster .swimlane-body${suffix}`;
sections += `
${slot}.cluster:not(.swimlane) rect {
stroke: ${borderColor};
${fill}
}
${slot}.cluster:not(.swimlane) path {
stroke: ${borderColor};
${fill}
}
/* Lane, classic and neo. */
${slot}.swimlane.cluster rect.swimlane-title,
${slot}.swimlane.cluster rect.swimlane-body {
stroke: ${borderColor};
${fill}
}
/* Lane, handDrawn: roughjs emits the hachure fill first, then the outline. */
${laneRule(' path:nth-of-type(2)')} {
stroke: ${borderColor};
}
${
hasBkgColors
? `
/* A roughjs fill is drawn as lines, so the lane fill is a stroke here. */
${laneRule(' path:first-of-type')} {
stroke: ${bkgColorArray[i % bkgColorArray.length]};
}
`
: ''
}
${collapsedRule('.collapsed-group')},
${collapsedRule('.collapsed-group path')} {
stroke: ${borderColor};
${fill}
}
/* The ellipsis dots and the separator take clusterBorder further down, so without
these the container is palette-coloured while its own markers are not. */
${collapsedRule('.collapsed-indicator')} {
fill: ${borderColor};
}
${collapsedRule('.collapsed-separator')} {
stroke: ${borderColor};
}
`;
}
return sections;
};
const fade = (color: string, opacity: number) => {
// @ts-ignore TODO: incorrect types from khroma
const channel = khroma.channel;
const r = channel(color, 'r');
const g = channel(color, 'g');
const b = channel(color, 'b');
// @ts-ignore incorrect types from khroma
return khroma.rgba(r, g, b, opacity);
};
const getStyles = (options: FlowChartStyleOptions) =>
`${genColor(options)}
.label {
font-family: ${options.fontFamily};
color: ${options.nodeTextColor || options.textColor};
}
.cluster-label text {
fill: ${options.titleColor};
}
.cluster-label span {
color: ${options.titleColor};
}
.cluster-label span p {
background-color: transparent;
}
.label text,span {
fill: ${options.nodeTextColor || options.textColor};
color: ${options.nodeTextColor || options.textColor};
}
.node rect,
.node circle,
.node ellipse,
.node polygon,
.node path {
fill: ${options.mainBkg};
stroke: ${options.nodeBorder};
stroke-width: ${options.strokeWidth ?? 1}px;
}
.rough-node .label text , .node .label text, .image-shape .label, .icon-shape .label {
text-anchor: middle;
}
.node .katex path {
fill: #000;
stroke: #000;
stroke-width: 1px;
}
.rough-node .label,.node .label, .image-shape .label, .icon-shape .label {
text-align: center;
}
.node.clickable {
cursor: pointer;
}
.root .anchor path {
fill: ${options.lineColor} !important;
stroke-width: 0;
stroke: ${options.lineColor};
}
.arrowheadPath {
fill: ${options.arrowheadColor};
}
.edgePaths .path {
stroke: ${options.lineColor};
stroke-width: ${options.strokeWidth ?? 2}px;
}
.flowchart-link {
stroke: ${options.lineColor};
fill: none;
}
.edgeLabel {
background-color: ${options.edgeLabelBackground};
p {
background-color: ${options.edgeLabelBackground};
}
rect {
opacity: 0.5;
background-color: ${options.edgeLabelBackground};
fill: ${options.edgeLabelBackground};
}
text-align: center;
}
/* For html labels only */
.labelBkg {
background-color: ${fade(options.edgeLabelBackground, 0.5)};
// background-color:
}
.cluster rect {
fill: ${options.clusterBkg};
stroke: ${options.clusterBorder};
stroke-width: 1px;
}
.cluster text {
fill: ${options.titleColor};
}
.cluster span {
color: ${options.titleColor};
}
/* .cluster div {
color: ${options.titleColor};
} */
/* Collapsed subgraph node (@{ view: collapsed }) */
.node .collapsed-indicator {
fill: ${options.clusterBorder};
stroke: none;
opacity: 0.6;
}
.node .collapsed-separator {
stroke: ${options.clusterBorder};
stroke-width: 0.75px;
}
div.mermaidTooltip {
position: absolute;
text-align: center;
max-width: 200px;
padding: 2px;
font-family: ${options.fontFamily};
font-size: 12px;
background: ${options.tertiaryColor};
border: 1px solid ${options.border2};
border-radius: 2px;
pointer-events: none;
z-index: 100;
}
.flowchartTitleText {
text-anchor: middle;
font-size: 18px;
fill: ${options.textColor};
}
rect.text {
fill: none;
stroke-width: 0;
}
.icon-shape, .image-shape {
background-color: ${options.edgeLabelBackground};
p {
background-color: ${options.edgeLabelBackground};
padding: 2px;
}
.label rect {
opacity: 0.5;
background-color: ${options.edgeLabelBackground};
fill: ${options.edgeLabelBackground};
}
text-align: center;
}
${getIconStyles()}
`;
export default getStyles;