Compare commits
43 Commits
b51f305a53
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ac0d7ee068 | ||
|
|
16e1078562 | ||
|
|
4cb2c7897e | ||
|
|
a4a23f2ed3 | ||
|
|
7432f09be9 | ||
|
|
d91ccb84b9 | ||
|
|
676f7689e0 | ||
|
|
e94aff9ca5 | ||
|
|
fd7ddd83ec | ||
|
|
e1051991a2 | ||
|
|
a9dc077e73 | ||
|
|
35d545c527 | ||
|
|
4bee68e6c4 | ||
|
|
02fb38fdda | ||
|
|
88a52d6bfc | ||
|
|
0e99eb925b | ||
|
|
9e152bc038 | ||
|
|
83c21f6f49 | ||
|
|
651843dbef | ||
|
|
9f253ce482 | ||
|
|
8be8525c02 | ||
|
|
707ab68523 | ||
|
|
8609ff2517 | ||
|
|
89253c8488 | ||
|
|
fc216494d4 | ||
|
|
ba1fe02294 | ||
|
|
207e858bce | ||
|
|
00a1be0c96 | ||
|
|
537d5a1b52 | ||
|
|
ef3f56ecf6 | ||
|
|
d393a1d8f3 | ||
|
|
be522c693a | ||
|
|
e9ce177940 | ||
|
|
1646724403 | ||
|
|
3421d8160d | ||
|
|
c1b7ba4561 | ||
|
|
436b9099a6 | ||
|
|
f7bb320dd2 | ||
|
|
1a29c78461 | ||
|
|
ffddcecf64 | ||
|
|
23819f4d1b | ||
|
|
0c57ededaa | ||
|
|
944dc67916 |
3
.gitmodules
vendored
Normal file
3
.gitmodules
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
[submodule "40 Extras/OOP/die_einen_da"]
|
||||||
|
path = 40 Extras/OOP/die_einen_da
|
||||||
|
url = git@collaborating.tuhh.de:e-24/courses/oop/exo/2026/g08/die_einen_da.git
|
||||||
3
.obsidian/app.json
vendored
3
.obsidian/app.json
vendored
@@ -8,5 +8,6 @@
|
|||||||
"margin": "2",
|
"margin": "2",
|
||||||
"downscalePercent": 100
|
"downscalePercent": 100
|
||||||
},
|
},
|
||||||
"promptDelete": false
|
"promptDelete": false,
|
||||||
|
"showUnsupportedFiles": true
|
||||||
}
|
}
|
||||||
8
.obsidian/appearance.json
vendored
8
.obsidian/appearance.json
vendored
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
"theme": "moonstone",
|
"theme": "obsidian",
|
||||||
"interfaceFontFamily": "Noto Sans",
|
"interfaceFontFamily": "Noto Sans",
|
||||||
"cssTheme": "Tokyo Night",
|
"cssTheme": "Minimal",
|
||||||
"enabledCssSnippets": [
|
"enabledCssSnippets": [
|
||||||
"export",
|
"export",
|
||||||
"matugen"
|
"matugen"
|
||||||
]
|
],
|
||||||
|
"accentColor": "#1c8728",
|
||||||
|
"baseFontSize": 22
|
||||||
}
|
}
|
||||||
4
.obsidian/community-plugins.json
vendored
4
.obsidian/community-plugins.json
vendored
@@ -1,6 +1,5 @@
|
|||||||
[
|
[
|
||||||
"obsidian-minimal-settings",
|
"obsidian-minimal-settings",
|
||||||
"obsidian-typst-cli",
|
|
||||||
"edit-in-neovim",
|
"edit-in-neovim",
|
||||||
"obsidian-git",
|
"obsidian-git",
|
||||||
"better-export-pdf",
|
"better-export-pdf",
|
||||||
@@ -11,5 +10,6 @@
|
|||||||
"dataview",
|
"dataview",
|
||||||
"obsidian-vimrc-support",
|
"obsidian-vimrc-support",
|
||||||
"emoji-shortcodes",
|
"emoji-shortcodes",
|
||||||
"execute-code"
|
"typst-mate",
|
||||||
|
"inline-math"
|
||||||
]
|
]
|
||||||
22
.obsidian/graph.json
vendored
22
.obsidian/graph.json
vendored
@@ -2,21 +2,21 @@
|
|||||||
"collapse-filter": true,
|
"collapse-filter": true,
|
||||||
"search": "",
|
"search": "",
|
||||||
"showTags": false,
|
"showTags": false,
|
||||||
"showAttachments": false,
|
"showAttachments": true,
|
||||||
"hideUnresolved": false,
|
"hideUnresolved": false,
|
||||||
"showOrphans": true,
|
"showOrphans": false,
|
||||||
"collapse-color-groups": true,
|
"collapse-color-groups": true,
|
||||||
"colorGroups": [],
|
"colorGroups": [],
|
||||||
"collapse-display": true,
|
"collapse-display": false,
|
||||||
"showArrow": false,
|
"showArrow": false,
|
||||||
"textFadeMultiplier": 0,
|
"textFadeMultiplier": -2.1,
|
||||||
"nodeSizeMultiplier": 1,
|
"nodeSizeMultiplier": 0.9421875,
|
||||||
"lineSizeMultiplier": 1,
|
"lineSizeMultiplier": 1.8609375,
|
||||||
"collapse-forces": false,
|
"collapse-forces": false,
|
||||||
"centerStrength": 0,
|
"centerStrength": 0.276041666666667,
|
||||||
"repelStrength": 0,
|
"repelStrength": 12.0833333333333,
|
||||||
"linkStrength": 0.500822368421053,
|
"linkStrength": 0.703125,
|
||||||
"linkDistance": 30,
|
"linkDistance": 101,
|
||||||
"scale": 0.9895212671709476,
|
"scale": 0.5034307057066698,
|
||||||
"close": true
|
"close": true
|
||||||
}
|
}
|
||||||
16
.obsidian/plugins/better-export-pdf/data.json
vendored
16
.obsidian/plugins/better-export-pdf/data.json
vendored
@@ -14,17 +14,17 @@
|
|||||||
"concurrency": "5",
|
"concurrency": "5",
|
||||||
"prevConfig": {
|
"prevConfig": {
|
||||||
"pageSize": "A4",
|
"pageSize": "A4",
|
||||||
"marginType": "1",
|
"marginType": "3",
|
||||||
"showTitle": false,
|
"showTitle": true,
|
||||||
"open": true,
|
"open": true,
|
||||||
"scale": 100,
|
"scale": 100,
|
||||||
"landscape": false,
|
"landscape": false,
|
||||||
"marginTop": "10",
|
"marginTop": "3.5",
|
||||||
"marginBottom": "10",
|
"marginBottom": "2",
|
||||||
"marginLeft": "10",
|
"marginLeft": "3.5",
|
||||||
"marginRight": "10",
|
"marginRight": "2",
|
||||||
"displayHeader": true,
|
"displayHeader": false,
|
||||||
"displayFooter": true,
|
"displayFooter": false,
|
||||||
"cssSnippet": "0"
|
"cssSnippet": "0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
6
.obsidian/plugins/inline-math/data.json
vendored
Normal file
6
.obsidian/plugins/inline-math/data.json
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"disableInTable": false,
|
||||||
|
"disableOnIME": true,
|
||||||
|
"disableDecorations": false,
|
||||||
|
"disableAtomicRanges": false
|
||||||
|
}
|
||||||
422
.obsidian/plugins/inline-math/main.js
vendored
Normal file
422
.obsidian/plugins/inline-math/main.js
vendored
Normal file
@@ -0,0 +1,422 @@
|
|||||||
|
/*
|
||||||
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
|
if you want to view the source, please visit the github repository of this plugin
|
||||||
|
*/
|
||||||
|
|
||||||
|
var __defProp = Object.defineProperty;
|
||||||
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||||
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||||
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||||
|
var __export = (target, all) => {
|
||||||
|
for (var name in all)
|
||||||
|
__defProp(target, name, { get: all[name], enumerable: true });
|
||||||
|
};
|
||||||
|
var __copyProps = (to, from, except, desc) => {
|
||||||
|
if (from && typeof from === "object" || typeof from === "function") {
|
||||||
|
for (let key of __getOwnPropNames(from))
|
||||||
|
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||||
|
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||||
|
}
|
||||||
|
return to;
|
||||||
|
};
|
||||||
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||||
|
|
||||||
|
// src/main.ts
|
||||||
|
var main_exports = {};
|
||||||
|
__export(main_exports, {
|
||||||
|
default: () => NoMoreFlicker
|
||||||
|
});
|
||||||
|
module.exports = __toCommonJS(main_exports);
|
||||||
|
var import_obsidian3 = require("obsidian");
|
||||||
|
|
||||||
|
// src/settings.ts
|
||||||
|
var import_obsidian = require("obsidian");
|
||||||
|
var DEFAULT_SETTINGS = {
|
||||||
|
disableInTable: false,
|
||||||
|
disableOnIME: true,
|
||||||
|
disableDecorations: false,
|
||||||
|
disableAtomicRanges: false
|
||||||
|
};
|
||||||
|
var NoMoreFlickerSettingTab = class extends import_obsidian.PluginSettingTab {
|
||||||
|
constructor(app, plugin) {
|
||||||
|
super(app, plugin);
|
||||||
|
this.plugin = plugin;
|
||||||
|
}
|
||||||
|
display() {
|
||||||
|
const { containerEl } = this;
|
||||||
|
containerEl.empty();
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Disable in tables").setDesc("If turned on, braces won't be inserted in tables. Decorations & atomic ranges are enabled regardless of this setting.").addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.disableInTable).onChange(async (disable) => {
|
||||||
|
this.plugin.settings.disableInTable = disable;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Disable when using IME input").setDesc("This option can be helpful for avoiding some strange behavior occurring when using IME inputs after escaping from a math block with the Latex Suite plugin's tabout feature.").addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.disableOnIME).onChange(async (disable) => {
|
||||||
|
this.plugin.settings.disableOnIME = disable;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Debug mode").setHeading();
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Disable decorations").setDesc("If turned on, decorations to hide braces adjacent to dollar signs are disabled. This is especially useful when you want to see what this plugin does under the hood.").addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.disableDecorations).onChange(async (disable) => {
|
||||||
|
this.plugin.settings.disableDecorations = disable;
|
||||||
|
this.plugin.remakeViewPlugin();
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(containerEl).setName("Disable atomic ranges").setDesc(createFragment((el) => {
|
||||||
|
el.createSpan({ text: 'If turned on, atomic ranges to treat each of "' });
|
||||||
|
el.createEl("code", { text: "${} " });
|
||||||
|
el.createSpan({ text: '" or "' });
|
||||||
|
el.createEl("code", { text: " {}$" });
|
||||||
|
el.createSpan({ text: '" as one character are disabled.' });
|
||||||
|
})).addToggle((toggle) => {
|
||||||
|
toggle.setValue(this.plugin.settings.disableAtomicRanges).onChange(async (disable) => {
|
||||||
|
this.plugin.settings.disableAtomicRanges = disable;
|
||||||
|
this.plugin.remakeViewPlugin();
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
new import_obsidian.Setting(containerEl).addButton((button) => {
|
||||||
|
button.setButtonText("Restore defaults").onClick(async () => {
|
||||||
|
this.plugin.settings = Object.assign({}, DEFAULT_SETTINGS);
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
this.display();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// src/decoration-and-atomic-range.ts
|
||||||
|
var import_state = require("@codemirror/state");
|
||||||
|
var import_view = require("@codemirror/view");
|
||||||
|
var import_language2 = require("@codemirror/language");
|
||||||
|
|
||||||
|
// src/utils.ts
|
||||||
|
var import_language = require("@codemirror/language");
|
||||||
|
var INLINE_MATH_BEGIN = "formatting_formatting-math_formatting-math-begin_keyword_math";
|
||||||
|
var MATH_END = "formatting_formatting-math_formatting-math-end_keyword_math_math-";
|
||||||
|
function nodeText(node, state) {
|
||||||
|
return state.sliceDoc(node.from, node.to);
|
||||||
|
}
|
||||||
|
function isInlineMathBegin(node, state) {
|
||||||
|
return node.name == INLINE_MATH_BEGIN && nodeText(node, state) == "$";
|
||||||
|
}
|
||||||
|
function isInlineMathEnd(node, state) {
|
||||||
|
return node.name == MATH_END && nodeText(node, state) == "$";
|
||||||
|
}
|
||||||
|
function selectionSatisfies(state, predicate) {
|
||||||
|
let ret = false;
|
||||||
|
const tree = (0, import_language.syntaxTree)(state);
|
||||||
|
for (const { from } of state.selection.ranges) {
|
||||||
|
const line = state.doc.lineAt(from);
|
||||||
|
tree.iterate({
|
||||||
|
from: line.from,
|
||||||
|
to: line.to,
|
||||||
|
enter: (node) => {
|
||||||
|
if (predicate(node)) {
|
||||||
|
ret = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/decoration-and-atomic-range.ts
|
||||||
|
var DummyRangeValue = class extends import_state.RangeValue {
|
||||||
|
};
|
||||||
|
var createViewPlugin = (plugin) => import_view.ViewPlugin.fromClass(
|
||||||
|
class {
|
||||||
|
constructor(view) {
|
||||||
|
this.impl(view);
|
||||||
|
}
|
||||||
|
update(update) {
|
||||||
|
this.impl(update.view);
|
||||||
|
}
|
||||||
|
impl(view) {
|
||||||
|
const decorationBulder = new import_state.RangeSetBuilder();
|
||||||
|
const atomicRangeBulder = new import_state.RangeSetBuilder();
|
||||||
|
const tree = (0, import_language2.syntaxTree)(view.state);
|
||||||
|
for (const { from, to } of view.visibleRanges) {
|
||||||
|
tree.iterate({
|
||||||
|
from,
|
||||||
|
to,
|
||||||
|
enter(node) {
|
||||||
|
if (isInlineMathBegin(node, view.state)) {
|
||||||
|
if (view.state.sliceDoc(node.to, node.to + 3) == "{} ") {
|
||||||
|
decorationBulder.add(
|
||||||
|
node.to,
|
||||||
|
node.to + 3,
|
||||||
|
import_view.Decoration.replace({})
|
||||||
|
);
|
||||||
|
atomicRangeBulder.add(
|
||||||
|
node.from,
|
||||||
|
node.to + 3,
|
||||||
|
new DummyRangeValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else if (isInlineMathEnd(node, view.state)) {
|
||||||
|
if (view.state.sliceDoc(node.from - 3, node.from) == " {}") {
|
||||||
|
decorationBulder.add(
|
||||||
|
node.from - 3,
|
||||||
|
node.from,
|
||||||
|
import_view.Decoration.replace({})
|
||||||
|
);
|
||||||
|
atomicRangeBulder.add(
|
||||||
|
node.from - 3,
|
||||||
|
node.to,
|
||||||
|
new DummyRangeValue()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.decorations = decorationBulder.finish();
|
||||||
|
this.atomicRanges = atomicRangeBulder.finish();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
decorations: (instance) => plugin.settings.disableDecorations ? import_view.Decoration.none : instance.decorations,
|
||||||
|
provide: (viewPlugin) => import_view.EditorView.atomicRanges.of((view) => {
|
||||||
|
var _a, _b;
|
||||||
|
return plugin.settings.disableAtomicRanges ? import_state.RangeSet.empty : (_b = (_a = view.plugin(viewPlugin)) == null ? void 0 : _a.atomicRanges) != null ? _b : import_state.RangeSet.empty;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// src/transaction-filter.ts
|
||||||
|
var import_state3 = require("@codemirror/state");
|
||||||
|
var import_language4 = require("@codemirror/language");
|
||||||
|
|
||||||
|
// src/latex-suite.ts
|
||||||
|
var import_state2 = require("@codemirror/state");
|
||||||
|
var import_language3 = require("@codemirror/language");
|
||||||
|
function handleLatexSuite(tr, plugin) {
|
||||||
|
if (tr.docChanged && !tr.selection) {
|
||||||
|
const changes = handleLatexSuiteBoxing(tr.startState, tr.changes);
|
||||||
|
if (changes) {
|
||||||
|
plugin._latexSuiteBoxing = true;
|
||||||
|
return { changes };
|
||||||
|
}
|
||||||
|
} else if (!tr.docChanged && tr.selection) {
|
||||||
|
if (plugin._latexSuiteBoxing) {
|
||||||
|
plugin._latexSuiteBoxing = false;
|
||||||
|
return { selection: { anchor: tr.selection.main.anchor - 3 } };
|
||||||
|
} else {
|
||||||
|
const selection = handleLatexSuiteTabout(tr.startState, tr.selection);
|
||||||
|
return [tr, { selection }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function handleLatexSuiteTabout(state, newSelection) {
|
||||||
|
const tree = (0, import_language3.syntaxTree)(state);
|
||||||
|
const doc = state.doc.toString();
|
||||||
|
const newRanges = [];
|
||||||
|
for (const range of newSelection.ranges) {
|
||||||
|
const indexNextDollar = doc.indexOf("$", range.to);
|
||||||
|
if (indexNextDollar >= 0) {
|
||||||
|
const node = tree.cursorAt(indexNextDollar, 1).node;
|
||||||
|
if (range.from === range.to && range.to === indexNextDollar && isInlineMathEnd(node, state) && state.sliceDoc(node.from - 3, node.from) === " {}") {
|
||||||
|
newRanges.push(import_state2.EditorSelection.cursor(node.to));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newRanges.push(range);
|
||||||
|
}
|
||||||
|
return import_state2.EditorSelection.create(newRanges, newSelection.mainIndex);
|
||||||
|
}
|
||||||
|
function handleLatexSuiteBoxing(state, changes) {
|
||||||
|
const tree = (0, import_language3.syntaxTree)(state);
|
||||||
|
let changeToReplace;
|
||||||
|
changes.iterChanges((fromA, toA, fromB, toB, inserted) => {
|
||||||
|
if (inserted.toString() === "\\boxed{" + state.sliceDoc(fromA, toA) + "}") {
|
||||||
|
const nodeFrom = tree.cursorAt(fromA, -1).node;
|
||||||
|
const nodeTo = tree.cursorAt(toA, 1).node;
|
||||||
|
if (isInlineMathBegin(nodeFrom, state) && isInlineMathEnd(nodeTo, state)) {
|
||||||
|
if (state.sliceDoc(fromA, fromA + 3) === "{} " && state.sliceDoc(toA - 3, toA) === " {}") {
|
||||||
|
changeToReplace = { from: fromA, to: toA, insert: "\\boxed{" + state.sliceDoc(fromA + 3, toA - 3) + "}" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return changeToReplace;
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/transaction-filter.ts
|
||||||
|
var import_obsidian2 = require("obsidian");
|
||||||
|
var makeTransactionFilter = (plugin) => {
|
||||||
|
return import_state3.EditorState.transactionFilter.of((tr) => {
|
||||||
|
var _a;
|
||||||
|
if (plugin.shouldIgnore(tr.startState))
|
||||||
|
return tr;
|
||||||
|
const userEvent = (_a = tr.annotation(import_state3.Transaction.userEvent)) == null ? void 0 : _a.split(".")[0];
|
||||||
|
if (userEvent === "input") {
|
||||||
|
if (plugin.settings.disableOnIME) {
|
||||||
|
const view = tr.startState.field(import_obsidian2.editorEditorField);
|
||||||
|
if (view.composing)
|
||||||
|
return tr;
|
||||||
|
}
|
||||||
|
const changes = getChangesForInsertion(tr.startState, tr.changes);
|
||||||
|
return [tr, { changes }];
|
||||||
|
} else if (userEvent === "select" && tr.selection) {
|
||||||
|
const changes = getChangesForSelection(tr.startState, tr.selection);
|
||||||
|
return [tr, { changes }];
|
||||||
|
} else if (userEvent === "delete") {
|
||||||
|
const changes = getChangesForDeletion(tr.startState);
|
||||||
|
return [tr, { changes }];
|
||||||
|
} else if (userEvent === void 0) {
|
||||||
|
const spec = handleLatexSuite(tr, plugin);
|
||||||
|
if (spec)
|
||||||
|
return spec;
|
||||||
|
}
|
||||||
|
return tr;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
function getChangesForDeletion(state) {
|
||||||
|
const tree = (0, import_language4.syntaxTree)(state);
|
||||||
|
const doc = state.doc.toString();
|
||||||
|
const changes = [];
|
||||||
|
for (const range of state.selection.ranges) {
|
||||||
|
const from = range.empty ? range.from - 4 : range.from;
|
||||||
|
const to = range.to;
|
||||||
|
const text = state.sliceDoc(from, to);
|
||||||
|
const index = text.lastIndexOf("$");
|
||||||
|
if (index == -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const indexNextDollar = doc.indexOf("$", from + index + 1);
|
||||||
|
const indexPrevDollar = doc.lastIndexOf("$", from);
|
||||||
|
tree.iterate({
|
||||||
|
from: indexPrevDollar,
|
||||||
|
to: indexNextDollar >= 0 ? indexNextDollar : to,
|
||||||
|
enter(node) {
|
||||||
|
if (isInlineMathBegin(node, state) && state.sliceDoc(node.to, node.to + 3) == "{} ") {
|
||||||
|
changes.push({ from: node.to, to: node.to + 3 });
|
||||||
|
} else if (isInlineMathEnd(node, state) && state.sliceDoc(node.from - 3, node.from) == " {}") {
|
||||||
|
changes.push({ from: node.from - 3, to: node.from });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
function getChangesForInsertion(state, changes) {
|
||||||
|
const tree = (0, import_language4.syntaxTree)(state);
|
||||||
|
const doc = state.doc.toString();
|
||||||
|
const changesToAdd = [];
|
||||||
|
const beginningOfChanges = /* @__PURE__ */ new Set();
|
||||||
|
changes.iterChangedRanges((fromA, toA, fromB, toB) => {
|
||||||
|
beginningOfChanges.add(fromA);
|
||||||
|
});
|
||||||
|
for (const range of state.selection.ranges) {
|
||||||
|
if (range.from >= 1) {
|
||||||
|
const indexPrevDollar = doc.lastIndexOf("$", range.from - 1);
|
||||||
|
if (indexPrevDollar >= 0) {
|
||||||
|
const node = tree.cursorAt(indexPrevDollar, 1).node;
|
||||||
|
if (isInlineMathBegin(node, state)) {
|
||||||
|
if (indexPrevDollar === range.from - 1 && beginningOfChanges.has(range.from)) {
|
||||||
|
changesToAdd.push({ from: indexPrevDollar, to: range.from, insert: "${} " });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (state.sliceDoc(node.to, node.to + 3) !== "{} ") {
|
||||||
|
changesToAdd.push({ from: node.to, insert: "{} " });
|
||||||
|
}
|
||||||
|
} else if (isInlineMathEnd(node, state) && state.sliceDoc(node.from - 3, node.from) === " {}") {
|
||||||
|
const openIndex = doc.lastIndexOf("${} ", node.from - 3);
|
||||||
|
changesToAdd.push({ from: openIndex + 1, to: node.from, insert: doc.slice(openIndex + 4, node.from - 3).trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const indexNextDollar = doc.indexOf("$", range.to);
|
||||||
|
if (indexNextDollar >= 0) {
|
||||||
|
const node = tree.cursorAt(indexNextDollar, 1).node;
|
||||||
|
if (isInlineMathEnd(node, state)) {
|
||||||
|
if (state.sliceDoc(node.from - 3, node.from) !== " {}") {
|
||||||
|
changesToAdd.push({ from: node.from, insert: " {}" });
|
||||||
|
}
|
||||||
|
} else if (isInlineMathBegin(node, state) && state.sliceDoc(node.to, node.to + 3) === "{} ") {
|
||||||
|
const closeIndex = doc.indexOf(" {}$", node.to + 3);
|
||||||
|
if (closeIndex >= 0) {
|
||||||
|
changesToAdd.push({ from: node.to, to: closeIndex + 3, insert: doc.slice(node.to + 3, closeIndex).trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changesToAdd;
|
||||||
|
}
|
||||||
|
function getChangesForSelection(state, newSelection) {
|
||||||
|
const tree = (0, import_language4.syntaxTree)(state);
|
||||||
|
const doc = state.doc.toString();
|
||||||
|
const changes = [];
|
||||||
|
for (let i = 0; i < newSelection.ranges.length; i++) {
|
||||||
|
const range = newSelection.ranges[i];
|
||||||
|
const indexNextDollar = doc.indexOf("$", range.to);
|
||||||
|
const indexPrevDollar = doc.lastIndexOf("$", range.from - 1);
|
||||||
|
if (indexPrevDollar >= 0) {
|
||||||
|
const node = tree.cursorAt(indexPrevDollar, 1).node;
|
||||||
|
if (isInlineMathEnd(node, state) && state.sliceDoc(node.from - 3, node.from) === " {}") {
|
||||||
|
const openIndex = doc.lastIndexOf("${} ", node.from - 3);
|
||||||
|
changes.push({ from: openIndex + 1, to: node.from, insert: doc.slice(openIndex + 4, node.from - 3).trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (indexNextDollar >= 0) {
|
||||||
|
const node = tree.cursorAt(indexNextDollar, 1).node;
|
||||||
|
if (isInlineMathBegin(node, state) && state.sliceDoc(node.to, node.to + 3) === "{} ") {
|
||||||
|
const closeIndex = doc.indexOf(" {}$", node.to + 3);
|
||||||
|
if (closeIndex >= 0) {
|
||||||
|
changes.push({ from: node.to, to: closeIndex + 3, insert: doc.slice(node.to + 3, closeIndex).trim() });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return changes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// src/main.ts
|
||||||
|
var NoMoreFlicker = class extends import_obsidian3.Plugin {
|
||||||
|
constructor() {
|
||||||
|
super(...arguments);
|
||||||
|
/**
|
||||||
|
* a view plugin that provides
|
||||||
|
* - decorations to hide braces adjacent to "$"s
|
||||||
|
* - & atomic ranges to treat each of "${} " and " {}$" as one character
|
||||||
|
*/
|
||||||
|
this.viewPlugin = [];
|
||||||
|
/**
|
||||||
|
* Indicates whether the previous transaction was the first of the two transactions
|
||||||
|
* (1. text replacement & 2. cursor position change) that Latex Suite's "box current equation"
|
||||||
|
* command produces or not. See the commend in the makeTransactionFilter() method for details.
|
||||||
|
*/
|
||||||
|
this._latexSuiteBoxing = false;
|
||||||
|
}
|
||||||
|
async onload() {
|
||||||
|
await this.loadSettings();
|
||||||
|
await this.saveSettings();
|
||||||
|
this.addSettingTab(new NoMoreFlickerSettingTab(this.app, this));
|
||||||
|
this.registerEditorExtension(this.viewPlugin);
|
||||||
|
this.remakeViewPlugin();
|
||||||
|
this.registerEditorExtension(makeTransactionFilter(this));
|
||||||
|
}
|
||||||
|
async loadSettings() {
|
||||||
|
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData());
|
||||||
|
}
|
||||||
|
async saveSettings() {
|
||||||
|
await this.saveData(this.settings);
|
||||||
|
}
|
||||||
|
shouldIgnore(state) {
|
||||||
|
return this.settings.disableInTable && selectionSatisfies(
|
||||||
|
state,
|
||||||
|
(node) => node.name.includes("HyperMD-table") || node.name.includes("hmd-table")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
remakeViewPlugin() {
|
||||||
|
this.viewPlugin.length = 0;
|
||||||
|
this.viewPlugin.push(createViewPlugin(this));
|
||||||
|
this.app.workspace.updateOptions();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* nosourcemap */
|
||||||
15
.obsidian/plugins/inline-math/manifest.json
vendored
Normal file
15
.obsidian/plugins/inline-math/manifest.json
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"id": "inline-math",
|
||||||
|
"name": "No more flickering inline math",
|
||||||
|
"version": "0.3.6",
|
||||||
|
"minAppVersion": "1.3.0",
|
||||||
|
"description": "Remove flickering inline math.",
|
||||||
|
"author": "Ryota Ushio",
|
||||||
|
"authorUrl": "https://github.com/RyotaUshio",
|
||||||
|
"fundingUrl": {
|
||||||
|
"GitHub Sponsor": "https://github.com/sponsors/RyotaUshio",
|
||||||
|
"Buy Me a Coffee": "https://www.buymeacoffee.com/ryotaushio",
|
||||||
|
"Ko-fi": "https://ko-fi.com/ryotaushio"
|
||||||
|
},
|
||||||
|
"isDesktopOnly": false
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@
|
|||||||
"lineWidth": 40,
|
"lineWidth": 40,
|
||||||
"lineWidthWide": 50,
|
"lineWidthWide": 50,
|
||||||
"maxWidth": 88,
|
"maxWidth": 88,
|
||||||
"textNormal": 16,
|
"textNormal": 22,
|
||||||
"textSmall": 13,
|
"textSmall": 13,
|
||||||
"imgGrid": false,
|
"imgGrid": false,
|
||||||
"imgWidth": "img-default-width",
|
"imgWidth": "img-default-width",
|
||||||
|
|||||||
@@ -17,6 +17,10 @@
|
|||||||
{
|
{
|
||||||
"folder": "/",
|
"folder": "/",
|
||||||
"template": "99 Templates/New File Template.md"
|
"template": "99 Templates/New File Template.md"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"folder": "11 Homework/Mathe II",
|
||||||
|
"template": "99 Templates/Math Homework Template.md"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"enable_file_templates": false,
|
"enable_file_templates": false,
|
||||||
|
|||||||
BIN
.obsidian/plugins/typst-mate/caches/preview_elembic_1.1.1.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_elembic_1.1.1.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_lilaq_0.5.0.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_lilaq_0.5.0.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_mannot_0.3.1.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_mannot_0.3.1.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_quick-maths_0.2.1.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_quick-maths_0.2.1.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.1.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.1.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.2.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_tiptoe_0.3.2.cache
vendored
Normal file
Binary file not shown.
BIN
.obsidian/plugins/typst-mate/caches/preview_zero_0.5.0.cache
vendored
Normal file
BIN
.obsidian/plugins/typst-mate/caches/preview_zero_0.5.0.cache
vendored
Normal file
Binary file not shown.
217
.obsidian/plugins/typst-mate/data.json
vendored
Normal file
217
.obsidian/plugins/typst-mate/data.json
vendored
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
{
|
||||||
|
"concealMathSymbols": true,
|
||||||
|
"enableConcealMathSymbolRevealDelay": true,
|
||||||
|
"mathSymbolRevealDelay": 1000,
|
||||||
|
"complementSymbolWithUnicode": false,
|
||||||
|
"disableBracketHighlight": false,
|
||||||
|
"useObsidianTheme": true,
|
||||||
|
"enableInlinePreview": true,
|
||||||
|
"revertTabToDefault": false,
|
||||||
|
"jumpOutsideBracket": true,
|
||||||
|
"preferInlineExitForSingleLineDisplayMath": true,
|
||||||
|
"moveToEndOfMathBlockBeforeExiting": false,
|
||||||
|
"disableMacro": false,
|
||||||
|
"enableBackgroundRendering": true,
|
||||||
|
"patchPDFExport": false,
|
||||||
|
"autoBaseColor": true,
|
||||||
|
"baseColor": "#000000",
|
||||||
|
"offset": 0,
|
||||||
|
"fitToNoteWidthProfile": "Live",
|
||||||
|
"fitToNoteWidthProfiles": [
|
||||||
|
{
|
||||||
|
"name": "A3",
|
||||||
|
"width": "700pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "A4",
|
||||||
|
"width": "500pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "A5",
|
||||||
|
"width": "350pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Legal",
|
||||||
|
"width": "516pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Letter",
|
||||||
|
"width": "516pt"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Tabloid",
|
||||||
|
"width": "690pt"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"skipPreparationWaiting": false,
|
||||||
|
"disablePackageCache": false,
|
||||||
|
"preamble": "#set page(margin: 0pt, width: auto, height: auto)\n#show raw: set text(size: 1.25em)\n#set text(size: fontsize)\n#import \"@preview/mannot:0.3.1\": *\n#import \"@preview/quick-maths:0.2.1\": shorthands\n#show: shorthands.with(\n ($+-$, sym.plus.minus),\n ($|-$, math.tack),\n)",
|
||||||
|
"openTypstToolsOnStartup": true,
|
||||||
|
"enableMathjaxFallback": false,
|
||||||
|
"applyProcessorToMathJax": false,
|
||||||
|
"importPath": ".typst",
|
||||||
|
"enableDebugger": false,
|
||||||
|
"processor": {
|
||||||
|
"inline": {
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"id": "ce",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#import \"@preview/typsium:0.3.1\": ce\n#show math.equation: set text(font: (\"New Computer Modern Math\", \"Noto Serif CJK SC\"))\n#ce[{CODE}]",
|
||||||
|
"styling": "inline",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "tex",
|
||||||
|
"renderingEngine": "mathjax",
|
||||||
|
"format": "",
|
||||||
|
"styling": "inline",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "display",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#set page(margin: (x: 0pt, y: 0.3125em))\n#math.equation($ {CODE} $, block: false)",
|
||||||
|
"styling": "inline",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 1,
|
||||||
|
"useReplaceAll": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#set page(margin: (x: 0pt, y: 0.3125em))\n${CODE}$",
|
||||||
|
"styling": "inline",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 1,
|
||||||
|
"useReplaceAll": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"display": {
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"id": "block",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "$ {CODE} $",
|
||||||
|
"styling": "block",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 1,
|
||||||
|
"useReplaceAll": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "$ {CODE} $",
|
||||||
|
"styling": "block-center",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 1,
|
||||||
|
"useReplaceAll": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"codeblock": {
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"id": "typst",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "{CODE}",
|
||||||
|
"styling": "block-center",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": true,
|
||||||
|
"syntaxMode": 0,
|
||||||
|
"useReplaceAll": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "fletcher",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#import \"@preview/fletcher:0.5.8\" as fletcher: diagram, node, edge\n{CODE}",
|
||||||
|
"styling": "block-center",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 0,
|
||||||
|
"useReplaceAll": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lovelace",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#import \"@preview/lovelace:0.3.0\": *\n#pseudocode-list[\n{CODE}\n]",
|
||||||
|
"styling": "block",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 0,
|
||||||
|
"useReplaceAll": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "lilaq",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#import \"@preview/lilaq:0.5.0\" as lq\n{CODE}",
|
||||||
|
"styling": "block-center",
|
||||||
|
"noPreamble": false,
|
||||||
|
"fitToNoteWidth": false,
|
||||||
|
"syntaxMode": 0,
|
||||||
|
"useReplaceAll": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"excalidraw": {
|
||||||
|
"processors": [
|
||||||
|
{
|
||||||
|
"id": "default",
|
||||||
|
"renderingEngine": "typst-svg",
|
||||||
|
"format": "#set page(margin: 0.25em)\n${CODE}$",
|
||||||
|
"styling": "default",
|
||||||
|
"noPreamble": false,
|
||||||
|
"syntaxMode": 1,
|
||||||
|
"useReplaceAll": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"snippets": [
|
||||||
|
{
|
||||||
|
"category": "Matrix",
|
||||||
|
"name": "mat",
|
||||||
|
"description": "e.g. mat(3,3)@",
|
||||||
|
"kind": "display",
|
||||||
|
"id": "",
|
||||||
|
"content": "const parts = input.split(\",\").map(s => s.trim());\n\nconst [x, y] = parts.map(Number)\n\nconst rowText = `${(\"#CURSOR, \".repeat(x)).slice(0, -2)} ;\\n`;\nconst contentText = ` ${rowText}`.repeat(y);\n\nreturn `mat(\\n${contentText})`;",
|
||||||
|
"script": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Matrix",
|
||||||
|
"name": "matInline",
|
||||||
|
"description": "e.g. mat(3,3)@",
|
||||||
|
"kind": "inline",
|
||||||
|
"id": "",
|
||||||
|
"content": "const parts = input.split(\",\").map(s => s.trim());\n\nconst [x, y] = parts.map(Number)\n\nconst rowText = `${(\"#CURSOR, \".repeat(x)).slice(0, -2)} ;`;\nconst contentText = `${rowText}`.repeat(y);\n\nreturn `mat(${contentText})`;",
|
||||||
|
"script": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Cases",
|
||||||
|
"name": "cases",
|
||||||
|
"description": "",
|
||||||
|
"kind": "display",
|
||||||
|
"id": "",
|
||||||
|
"content": "cases(#CURSOR \"if\" #CURSOR, #CURSOR \"else\")",
|
||||||
|
"script": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"category": "Cases",
|
||||||
|
"name": "casesn",
|
||||||
|
"description": "e.g. casesn(3)@",
|
||||||
|
"kind": "display",
|
||||||
|
"id": "",
|
||||||
|
"content": "const n = Number(input);\nreturn `cases(\\n${(` #CURSOR \"if\" #CURSOR,\\n`).repeat(n-1)} #CURSOR \"else\"\\n)`",
|
||||||
|
"script": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"crashCount": 0
|
||||||
|
}
|
||||||
27
.obsidian/plugins/typst-mate/main.js
vendored
Normal file
27
.obsidian/plugins/typst-mate/main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
12
.obsidian/plugins/typst-mate/manifest.json
vendored
Normal file
12
.obsidian/plugins/typst-mate/manifest.json
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"id": "typst-mate",
|
||||||
|
"name": "Typst Mate",
|
||||||
|
"version": "2.3.2",
|
||||||
|
"minAppVersion": "1.0.0",
|
||||||
|
"description": "Render math expressions with Typst instead of MathJax.",
|
||||||
|
"author": "azyarashi",
|
||||||
|
"isDesktopOnly": false,
|
||||||
|
"fundingUrl": {
|
||||||
|
"Buy me a Coffee": "https://www.buymeacoffee.com/azyarashi"
|
||||||
|
}
|
||||||
|
}
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
MIT OR Apache-2.0
|
||||||
201
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-APACHE
vendored
Normal file
201
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-APACHE
vendored
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
Apache License
|
||||||
|
Version 2.0, January 2004
|
||||||
|
http://www.apache.org/licenses/
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||||
|
|
||||||
|
1. Definitions.
|
||||||
|
|
||||||
|
"License" shall mean the terms and conditions for use, reproduction,
|
||||||
|
and distribution as defined by Sections 1 through 9 of this document.
|
||||||
|
|
||||||
|
"Licensor" shall mean the copyright owner or entity authorized by
|
||||||
|
the copyright owner that is granting the License.
|
||||||
|
|
||||||
|
"Legal Entity" shall mean the union of the acting entity and all
|
||||||
|
other entities that control, are controlled by, or are under common
|
||||||
|
control with that entity. For the purposes of this definition,
|
||||||
|
"control" means (i) the power, direct or indirect, to cause the
|
||||||
|
direction or management of such entity, whether by contract or
|
||||||
|
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||||
|
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||||
|
|
||||||
|
"You" (or "Your") shall mean an individual or Legal Entity
|
||||||
|
exercising permissions granted by this License.
|
||||||
|
|
||||||
|
"Source" form shall mean the preferred form for making modifications,
|
||||||
|
including but not limited to software source code, documentation
|
||||||
|
source, and configuration files.
|
||||||
|
|
||||||
|
"Object" form shall mean any form resulting from mechanical
|
||||||
|
transformation or translation of a Source form, including but
|
||||||
|
not limited to compiled object code, generated documentation,
|
||||||
|
and conversions to other media types.
|
||||||
|
|
||||||
|
"Work" shall mean the work of authorship, whether in Source or
|
||||||
|
Object form, made available under the License, as indicated by a
|
||||||
|
copyright notice that is included in or attached to the work
|
||||||
|
(an example is provided in the Appendix below).
|
||||||
|
|
||||||
|
"Derivative Works" shall mean any work, whether in Source or Object
|
||||||
|
form, that is based on (or derived from) the Work and for which the
|
||||||
|
editorial revisions, annotations, elaborations, or other modifications
|
||||||
|
represent, as a whole, an original work of authorship. For the purposes
|
||||||
|
of this License, Derivative Works shall not include works that remain
|
||||||
|
separable from, or merely link (or bind by name) to the interfaces of,
|
||||||
|
the Work and Derivative Works thereof.
|
||||||
|
|
||||||
|
"Contribution" shall mean any work of authorship, including
|
||||||
|
the original version of the Work and any modifications or additions
|
||||||
|
to that Work or Derivative Works thereof, that is intentionally
|
||||||
|
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||||
|
or by an individual or Legal Entity authorized to submit on behalf of
|
||||||
|
the copyright owner. For the purposes of this definition, "submitted"
|
||||||
|
means any form of electronic, verbal, or written communication sent
|
||||||
|
to the Licensor or its representatives, including but not limited to
|
||||||
|
communication on electronic mailing lists, source code control systems,
|
||||||
|
and issue tracking systems that are managed by, or on behalf of, the
|
||||||
|
Licensor for the purpose of discussing and improving the Work, but
|
||||||
|
excluding communication that is conspicuously marked or otherwise
|
||||||
|
designated in writing by the copyright owner as "Not a Contribution."
|
||||||
|
|
||||||
|
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||||
|
on behalf of whom a Contribution has been received by Licensor and
|
||||||
|
subsequently incorporated within the Work.
|
||||||
|
|
||||||
|
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
copyright license to reproduce, prepare Derivative Works of,
|
||||||
|
publicly display, publicly perform, sublicense, and distribute the
|
||||||
|
Work and such Derivative Works in Source or Object form.
|
||||||
|
|
||||||
|
3. Grant of Patent License. Subject to the terms and conditions of
|
||||||
|
this License, each Contributor hereby grants to You a perpetual,
|
||||||
|
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||||
|
(except as stated in this section) patent license to make, have made,
|
||||||
|
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||||
|
where such license applies only to those patent claims licensable
|
||||||
|
by such Contributor that are necessarily infringed by their
|
||||||
|
Contribution(s) alone or by combination of their Contribution(s)
|
||||||
|
with the Work to which such Contribution(s) was submitted. If You
|
||||||
|
institute patent litigation against any entity (including a
|
||||||
|
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||||
|
or a Contribution incorporated within the Work constitutes direct
|
||||||
|
or contributory patent infringement, then any patent licenses
|
||||||
|
granted to You under this License for that Work shall terminate
|
||||||
|
as of the date such litigation is filed.
|
||||||
|
|
||||||
|
4. Redistribution. You may reproduce and distribute copies of the
|
||||||
|
Work or Derivative Works thereof in any medium, with or without
|
||||||
|
modifications, and in Source or Object form, provided that You
|
||||||
|
meet the following conditions:
|
||||||
|
|
||||||
|
(a) You must give any other recipients of the Work or
|
||||||
|
Derivative Works a copy of this License; and
|
||||||
|
|
||||||
|
(b) You must cause any modified files to carry prominent notices
|
||||||
|
stating that You changed the files; and
|
||||||
|
|
||||||
|
(c) You must retain, in the Source form of any Derivative Works
|
||||||
|
that You distribute, all copyright, patent, trademark, and
|
||||||
|
attribution notices from the Source form of the Work,
|
||||||
|
excluding those notices that do not pertain to any part of
|
||||||
|
the Derivative Works; and
|
||||||
|
|
||||||
|
(d) If the Work includes a "NOTICE" text file as part of its
|
||||||
|
distribution, then any Derivative Works that You distribute must
|
||||||
|
include a readable copy of the attribution notices contained
|
||||||
|
within such NOTICE file, excluding those notices that do not
|
||||||
|
pertain to any part of the Derivative Works, in at least one
|
||||||
|
of the following places: within a NOTICE text file distributed
|
||||||
|
as part of the Derivative Works; within the Source form or
|
||||||
|
documentation, if provided along with the Derivative Works; or,
|
||||||
|
within a display generated by the Derivative Works, if and
|
||||||
|
wherever such third-party notices normally appear. The contents
|
||||||
|
of the NOTICE file are for informational purposes only and
|
||||||
|
do not modify the License. You may add Your own attribution
|
||||||
|
notices within Derivative Works that You distribute, alongside
|
||||||
|
or as an addendum to the NOTICE text from the Work, provided
|
||||||
|
that such additional attribution notices cannot be construed
|
||||||
|
as modifying the License.
|
||||||
|
|
||||||
|
You may add Your own copyright statement to Your modifications and
|
||||||
|
may provide additional or different license terms and conditions
|
||||||
|
for use, reproduction, or distribution of Your modifications, or
|
||||||
|
for any such Derivative Works as a whole, provided Your use,
|
||||||
|
reproduction, and distribution of the Work otherwise complies with
|
||||||
|
the conditions stated in this License.
|
||||||
|
|
||||||
|
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||||
|
any Contribution intentionally submitted for inclusion in the Work
|
||||||
|
by You to the Licensor shall be under the terms and conditions of
|
||||||
|
this License, without any additional terms or conditions.
|
||||||
|
Notwithstanding the above, nothing herein shall supersede or modify
|
||||||
|
the terms of any separate license agreement you may have executed
|
||||||
|
with Licensor regarding such Contributions.
|
||||||
|
|
||||||
|
6. Trademarks. This License does not grant permission to use the trade
|
||||||
|
names, trademarks, service marks, or product names of the Licensor,
|
||||||
|
except as required for reasonable and customary use in describing the
|
||||||
|
origin of the Work and reproducing the content of the NOTICE file.
|
||||||
|
|
||||||
|
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||||
|
agreed to in writing, Licensor provides the Work (and each
|
||||||
|
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||||
|
implied, including, without limitation, any warranties or conditions
|
||||||
|
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||||
|
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||||
|
appropriateness of using or redistributing the Work and assume any
|
||||||
|
risks associated with Your exercise of permissions under this License.
|
||||||
|
|
||||||
|
8. Limitation of Liability. In no event and under no legal theory,
|
||||||
|
whether in tort (including negligence), contract, or otherwise,
|
||||||
|
unless required by applicable law (such as deliberate and grossly
|
||||||
|
negligent acts) or agreed to in writing, shall any Contributor be
|
||||||
|
liable to You for damages, including any direct, indirect, special,
|
||||||
|
incidental, or consequential damages of any character arising as a
|
||||||
|
result of this License or out of the use or inability to use the
|
||||||
|
Work (including but not limited to damages for loss of goodwill,
|
||||||
|
work stoppage, computer failure or malfunction, or any and all
|
||||||
|
other commercial damages or losses), even if such Contributor
|
||||||
|
has been advised of the possibility of such damages.
|
||||||
|
|
||||||
|
9. Accepting Warranty or Additional Liability. While redistributing
|
||||||
|
the Work or Derivative Works thereof, You may choose to offer,
|
||||||
|
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||||
|
or other liability obligations and/or rights consistent with this
|
||||||
|
License. However, in accepting such obligations, You may act only
|
||||||
|
on Your own behalf and on Your sole responsibility, not on behalf
|
||||||
|
of any other Contributor, and only if You agree to indemnify,
|
||||||
|
defend, and hold each Contributor harmless for any liability
|
||||||
|
incurred by, or claims asserted against, such Contributor by reason
|
||||||
|
of your accepting any such warranty or additional liability.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
APPENDIX: How to apply the Apache License to your work.
|
||||||
|
|
||||||
|
To apply the Apache License to your work, attach the following
|
||||||
|
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||||
|
replaced with your own identifying information. (Don't include
|
||||||
|
the brackets!) The text should be enclosed in the appropriate
|
||||||
|
comment syntax for the file format. We also recommend that a
|
||||||
|
file or class name and description of purpose be included on the
|
||||||
|
same "printed page" as the copyright notice for easier
|
||||||
|
identification within third-party archives.
|
||||||
|
|
||||||
|
Copyright 2024 PgBiel
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
21
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-MIT
vendored
Normal file
21
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/LICENSE-MIT
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 PgBiel
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
114
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/README.md
vendored
Normal file
114
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/README.md
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
# elembic
|
||||||
|
Framework for custom elements and types in Typst. **Supports Typst 0.11.0 or later.**
|
||||||
|
|
||||||
|
**Read the book:** https://pgbiel.github.io/elembic
|
||||||
|
|
||||||
|
**Mirrors:** [GitHub (pgbiel/elembic)](https://github.com/PgBiel/elembic); [Codeberg (pgbiel/elembic)](https://codeberg.org/PgBiel/elembic)
|
||||||
|
|
||||||
|
## About
|
||||||
|
|
||||||
|
Elembic lets you create [**custom elements,**](https://pgbiel.github.io/elembic/elements/creating/index.html) which are **reusable and customizable document components,** with support for **typechecked fields, [show and set rules](https://pgbiel.github.io/elembic/elements/styling/index.html)** (without using any `state` by default!), **[reference and outline support,](https://pgbiel.github.io/elembic/elements/creating/labels-refs.html)** as well as features not present in Typst elements today such as **[revokable rules](https://pgbiel.github.io/elembic/elements/styling/revoke.html)** and **[nested element selectors](https://pgbiel.github.io/elembic/elements/filters/within.html)**. In addition, Elembic lets you create [**custom types,**](https://pgbiel.github.io/elembic/types/custom-types/index.html) which also support **typechecked fields** but also [**custom casting**](https://pgbiel.github.io/elembic/types/custom-types/casts.html), and can be used in element fields or by themselves in arbitrary Typst code.
|
||||||
|
|
||||||
|
Elembic's name comes from "element" + ["alembic"](https://en.wikipedia.org/wiki/Alembic), to indicate that one of Elembic's goals is to experiment with different approaches for this functionality, and to help shape a future update to Typst that includes native custom elements, which will eventually remove the need for using this package.
|
||||||
|
|
||||||
|
Its documentation is at the Elembic Handbook: [https://pgbiel.github.io/elembic](https://pgbiel.github.io/elembic)
|
||||||
|
|
||||||
|
This library has a few limitations which are [appropriately noted in the book.](https://pgbiel.github.io/elembic/about/limitations.html)
|
||||||
|
|
||||||
|
## Who is elembic for?
|
||||||
|
|
||||||
|
Elembic is especially suited for:
|
||||||
|
1. **Packages:** elembic's elements allows creating reusable components which can be freely customized by your package's end users. With typechecking and other features, elembic's got you covered in terms of flexibility. See the ["Simple Theorems" example](https://pgbiel.github.io/elembic/examples/simple-theorems.html) for a sample.
|
||||||
|
2. **Templates:** elembic's elements can be used for fine-grained configuration of parts of your template. See the ["Simple Thesis" example](https://pgbiel.github.io/elembic/examples/simple-thesis.html) for a sample.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
Just import the latest elembic version from the package manager and you're ready to go!
|
||||||
|
|
||||||
|
```typ
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
// See the full example below
|
||||||
|
#show: e.set_(element, stroke: red)
|
||||||
|
// ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## Example
|
||||||
|
|
||||||
|
### Custom element
|
||||||
|
|
||||||
|
```typ
|
||||||
|
#import "@preview/elembic:1.1.1" as e: field, types
|
||||||
|
|
||||||
|
#let bigbox = e.element.declare(
|
||||||
|
"bigbox",
|
||||||
|
prefix: "@preview/my-package,v1",
|
||||||
|
doc: "A fancy and customizable box.",
|
||||||
|
display: it => block(fill: it.fill, stroke: it.stroke, inset: 5pt, it.body),
|
||||||
|
fields: (
|
||||||
|
field("body", types.option(content), doc: "Box contents", required: true),
|
||||||
|
field("fill", types.option(types.paint), doc: "Box fill"),
|
||||||
|
field("stroke", types.option(stroke), doc: "Box border", default: red)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#bigbox[abc]
|
||||||
|
|
||||||
|
#show: e.set_(bigbox, fill: red)
|
||||||
|
|
||||||
|
#bigbox(stroke: blue + 2pt)[def]
|
||||||
|
```
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Custom type
|
||||||
|
|
||||||
|
```typ
|
||||||
|
#import "@preview/elembic:1.1.1" as e: field, types
|
||||||
|
|
||||||
|
#let person = e.types.declare(
|
||||||
|
"person",
|
||||||
|
prefix: "@preview/my-package,v1",
|
||||||
|
doc: "Relevant data for a person.",
|
||||||
|
fields: (
|
||||||
|
field("name", str, doc: "Person's name", required: true, named: true),
|
||||||
|
field("age", int, doc: "Person's age", default: 40),
|
||||||
|
field("preference", types.any, doc: "Anything the person likes", default: none)
|
||||||
|
),
|
||||||
|
casts: (
|
||||||
|
(from: dictionary),
|
||||||
|
(from: str, with: person => name => person(name: name)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
e.repr(person(name: "John", age: 50, preference: "soup")),
|
||||||
|
"person(age: 50, name: \"John\", preference: \"soup\")"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Manually invoke typechecking and cast
|
||||||
|
#assert.eq(
|
||||||
|
types.cast((name: "abc", age: 99), person),
|
||||||
|
(true, person(name: "abc", age: 99))
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
types.cast("abc", person),
|
||||||
|
(true, person(name: "abc"))
|
||||||
|
)
|
||||||
|
|
||||||
|
// Can then use 'person' as the type of an element's field, for example
|
||||||
|
```
|
||||||
|
|
||||||
|
## Source structure
|
||||||
|
|
||||||
|
- `pub/`: Contains public re-exports of each module (so we can keep some things private)
|
||||||
|
- `data`: Functions and constants related to extracting data from elements and types
|
||||||
|
- `element`: Functions related to creating and using elements and their rules (set rules, revoke rules and so on)
|
||||||
|
- `filters`: Functions to create and match element filters
|
||||||
|
- `types`: Functions and constants related to Elembic's custom type system
|
||||||
|
- `fields`: Functions related to element and type field parsing
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
Licensed under MIT or Apache-2.0, at your option.
|
||||||
584
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/data.typ
vendored
Normal file
584
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/data.typ
vendored
Normal file
@@ -0,0 +1,584 @@
|
|||||||
|
// Functions to extract data from custom elements and types, as well as associated constants.
|
||||||
|
|
||||||
|
// Type constants:
|
||||||
|
|
||||||
|
// Used by typeinfos
|
||||||
|
#let type-key = "__elembic_type"
|
||||||
|
// To be used by any custom type instances
|
||||||
|
#let custom-type-key = "__elembic_custom_type"
|
||||||
|
// Used by custom types themselves
|
||||||
|
#let custom-type-data-key = "__elembic_custom_type_data"
|
||||||
|
|
||||||
|
// Versions:
|
||||||
|
#let element-version = 5 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0, v4 = v1.1.0, v5 = v1.1.1+
|
||||||
|
#let type-version = 4 // v1 = alphas to v1.0.0-rc2, v2 = v1.0.0, v3 = v1.1.0, v4 = v1.1.1+
|
||||||
|
#let custom-type-version = 5 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0, v4 = v1.1.0, v5 = v1.1.1+
|
||||||
|
#let current-field-version = 4 // v1 = alphas 1 and 2, v2 = alpha 3-v1.0.0-rc2, v3 = v1.0.0-v1.1.0, v4 = v1.1.1+
|
||||||
|
|
||||||
|
// Potential modes for configuration of styles.
|
||||||
|
// This defines how we declare a set rule (or similar)
|
||||||
|
// within a certain scope.
|
||||||
|
#let style-modes = (
|
||||||
|
// Normal mode: we store metadata in a bibliography.title set rule.
|
||||||
|
//
|
||||||
|
// Before doing so, we retrieve the original value for bibliography.title,
|
||||||
|
// allowing us to restore it later. The effect is that the library is
|
||||||
|
// fully hygienic, that is, the change to bibliography.title is not perceptible.
|
||||||
|
//
|
||||||
|
// The downside is that retrieving the original value for bibliography.title costs
|
||||||
|
// an additional nested context { } call, of which there is a limit of 64. This means
|
||||||
|
// that, in this mode, you can have up to 32 non-consecutive set rules.
|
||||||
|
normal: 0,
|
||||||
|
|
||||||
|
// leaky mode: similar to normal mode, but we don't try to preserve the value of bibliography.title
|
||||||
|
// after applying our changes to the document. This doubles the limit to up to 64 non-consecutive
|
||||||
|
// set rules since we no longer have an extra step to retrieve the old value, but, as a downside,
|
||||||
|
// we lose the original value of bibliography.title. While, in a future change, we might be able to
|
||||||
|
// preserve the FIRST known value, we can't generally preserve its value at later points, so the
|
||||||
|
// value of bibliography.title is effectively frozen before the first custom set rule.
|
||||||
|
//
|
||||||
|
// This mode should be used by package authors which know there won't be a bibliography (or, really,
|
||||||
|
// any custom user input) at some point to avoid consuming the set rule cost. End users can also use
|
||||||
|
// this mode if they hit a "max show rule depth exceeded" error.
|
||||||
|
//
|
||||||
|
// Note that this mode can only be enabled on individual set rules.
|
||||||
|
leaky: 1,
|
||||||
|
|
||||||
|
// Stateful mode: this is entirely different from the other modes and should only be set by the end
|
||||||
|
// user (not by packages). This stores the style chain - and, thus, set rules' updated fields - in
|
||||||
|
// a 'state()'. This is more likely to be slower and lead to trouble as it triggers at least one
|
||||||
|
// document relayout. However, **this mode does not have a set rule limit.** Therefore, it can be
|
||||||
|
// used as a last resort by the end user if they can't fix the "max show rule depth exceeded error".
|
||||||
|
//
|
||||||
|
// Enabling this mode is as simple as using `#show: e.stateful.toggle(true)` at the beginning of the
|
||||||
|
// document. This will trigger a compatibility behavior where existing set rules will push to the
|
||||||
|
// state, even if they're not in the stateful mode. It will also push existing set rule data into
|
||||||
|
// the style 'state()'. Therefore, existing set rules are compatible with stateful mode, but this
|
||||||
|
// only effectively fixes the error if the set rules are individually switched to stateful mode
|
||||||
|
// with `e.stateful.set_` instead of `e.set_`.
|
||||||
|
stateful: 2
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// When on stateful mode, this state holds the sequence of 'data' for each scope.
|
||||||
|
// The last element on the list is the "current" data.
|
||||||
|
#let style-state-key = "__elembic_element_state"
|
||||||
|
#let style-state = state(style-state-key, ())
|
||||||
|
|
||||||
|
// Element constants:
|
||||||
|
|
||||||
|
// Prefix for the labels added to shown elements.
|
||||||
|
#let lbl-show-head = "__elembic_element_shown_"
|
||||||
|
|
||||||
|
// Prefix for the labels added to the metadata of each element.
|
||||||
|
// Used for querying.
|
||||||
|
#let lbl-meta-head = "__elembic_element_meta_"
|
||||||
|
|
||||||
|
// Prefix for the labels added outside shown elements.
|
||||||
|
// This is used to be able to effectively apply show-set rules to them.
|
||||||
|
#let lbl-outer-head = "__elembic_element_outer_"
|
||||||
|
|
||||||
|
// Prefix for counters of elements.
|
||||||
|
// This is only used if the element isn't 'refable'.
|
||||||
|
#let lbl-counter-head = "__elembic_element_counter_"
|
||||||
|
|
||||||
|
// Prefix for labels for 'context' which should panic with 'missing prepare(elem)'.
|
||||||
|
#let lbl-elem-prepare-check-head = "__elembic_elem_prepare_check_"
|
||||||
|
|
||||||
|
// Kind of the special figure used by a labelable element.
|
||||||
|
#let labelable-elem-figure-kind = "__elembic_element_labelable_figure"
|
||||||
|
|
||||||
|
// Prefix for the figure kind used by 'refable' elements.
|
||||||
|
// This is not to be confused with figures containing the elements.
|
||||||
|
// This is the kind for a hidden figure used for ref purposes.
|
||||||
|
#let lbl-ref-figure-kind-head = "__elembic_element_refable_"
|
||||||
|
|
||||||
|
// Custom label applied to the hidden reference figure when the user specifies their own label.
|
||||||
|
#let lbl-ref-figure-label-head = "__elembic_element_ref_figure_label_"
|
||||||
|
|
||||||
|
// Label for a context which should panic with 'missing prepare()'...
|
||||||
|
#let lbl-empty-prepare-check = <__elembic_empty_prepare_check>
|
||||||
|
|
||||||
|
// Label for the hidden figure used for references.
|
||||||
|
#let lbl-ref-figure = <__elembic_element_ref_figure>
|
||||||
|
|
||||||
|
// Label for context blocks which have access to the virtual stylechain.
|
||||||
|
#let lbl-get = <__elembic_element_get>
|
||||||
|
|
||||||
|
// Label for metadata indicating an element's initial properties post-construction.
|
||||||
|
#let lbl-tag = <__elembic_element_tag>
|
||||||
|
|
||||||
|
// Label for metadata indicating a rule's parameters.
|
||||||
|
#let lbl-rule-tag = <__elembic_element_rule_v2>
|
||||||
|
|
||||||
|
// 'lbl-rule-tag' from older Elembic versions.
|
||||||
|
#let lbl-old-rule-tag = <__elembic_element_rule>
|
||||||
|
|
||||||
|
// Label for other functions which access or modify the style chain, namely
|
||||||
|
// 'get', 'select', 'debug-get', 'stateful.toggle'.
|
||||||
|
//
|
||||||
|
// This is attached to metadata and the 'special-rule-key' property
|
||||||
|
// indicates which kind of rule this is.
|
||||||
|
#let lbl-special-rule-tag = <__elembic_element_special_rule>
|
||||||
|
|
||||||
|
// Label for metadata which stores the global data.
|
||||||
|
// In practice, this label is never present in the document
|
||||||
|
// unless one accidentally leaks the 'bibliography.title'
|
||||||
|
// override from our workaround.
|
||||||
|
#let lbl-data-metadata = <__elembic_element_global_data_metadata>
|
||||||
|
|
||||||
|
#let lbl-stateful-mode = <__elembic_element_stateful_mode>
|
||||||
|
#let lbl-normal-mode = <__elembic_element_normal_mode>
|
||||||
|
#let lbl-leaky-mode = <__elembic_element_leaky_mode>
|
||||||
|
#let lbl-auto-mode = <__elembic_element_auto_mode>
|
||||||
|
|
||||||
|
// Prefix for labels added by 'select' to matched elements.
|
||||||
|
// These labels are not specific to eids.
|
||||||
|
#let lbl-global-select-head = "__elembic_element_global_select_"
|
||||||
|
|
||||||
|
// Special dictionary key to indicate this is a prepared rule.
|
||||||
|
#let prepared-rule-key = "__elembic-prepared-rule"
|
||||||
|
|
||||||
|
// Special dictionary key to indicate this is a "special rule"
|
||||||
|
// ('get', 'select', 'debug-get', 'stateful.toggle').
|
||||||
|
#let special-rule-key = "__elembic-special-rule"
|
||||||
|
|
||||||
|
// Special dictionary key which stores element context and other data.
|
||||||
|
#let stored-data-key = "__elembic_stored_element_data"
|
||||||
|
|
||||||
|
// Special dictionary key to indicate this is query metadata for an element.
|
||||||
|
#let element-meta-key = "__elembic_element_meta"
|
||||||
|
|
||||||
|
#let element-key = "__elembic_element"
|
||||||
|
#let element-data-key = "__elembic_element_data"
|
||||||
|
#let global-data-key = "__elembic_element_global_data"
|
||||||
|
#let filter-key = "__elembic_element_filter"
|
||||||
|
|
||||||
|
#let sequence = [].func()
|
||||||
|
#let styled = { set text(red); [a] }.func()
|
||||||
|
#let elem-funcs = (sequence, styled, figure)
|
||||||
|
|
||||||
|
// Special values that can be passed to a type or element's constructor to retrieve some data or show
|
||||||
|
// some behavior.
|
||||||
|
#let special-data-values = (
|
||||||
|
// Indicate that the constructor should return the type or element's data.
|
||||||
|
get-data: 0,
|
||||||
|
// Indicate that the constructor should return an element filter.
|
||||||
|
get-where: 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Extract data from a type's or element's constructor, as well as convert
|
||||||
|
// a custom type or element instance into a dictionary with keys such as body (for elements only),
|
||||||
|
// fields and func, allowing you to access its fields and information when given content (for elements)
|
||||||
|
// or the type instance (for types).
|
||||||
|
//
|
||||||
|
// When given content that is not a custom element, 'body' will be the given value,
|
||||||
|
// 'fields' will be 'body.fields()' and 'func' will be 'body.func()'.
|
||||||
|
//
|
||||||
|
// The returned 'data-kind' indicates which kind of data was retrieved.
|
||||||
|
#let data(it) = {
|
||||||
|
if type(it) == function {
|
||||||
|
it(__elembic_data: special-data-values.get-data)
|
||||||
|
} else if type(it) == dictionary {
|
||||||
|
if element-key in it {
|
||||||
|
(data-kind: "element", ..it)
|
||||||
|
} else if custom-type-data-key in it {
|
||||||
|
(data-kind: "custom-type-data", ..it)
|
||||||
|
} else if custom-type-key in it {
|
||||||
|
it.at(custom-type-key)
|
||||||
|
} else if stored-data-key in it {
|
||||||
|
it.at(stored-data-key)
|
||||||
|
} else {
|
||||||
|
(data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false)
|
||||||
|
}
|
||||||
|
} else if type(it) != content {
|
||||||
|
(data-kind: "unknown", body: it, fields: (:), func: none, eid: none, fields-known: false, valid: false)
|
||||||
|
} else if (
|
||||||
|
it.func() == styled
|
||||||
|
and it.has("label")
|
||||||
|
and it.child.func() == sequence
|
||||||
|
and it.child.children.len() >= 2
|
||||||
|
and it.child.children.last().at("label", default: none) == lbl-tag
|
||||||
|
) {
|
||||||
|
// Any labeled elements need to be wrapped in a 'styled' to avoid
|
||||||
|
// interference when joining with the element in a show rule on the
|
||||||
|
// label.
|
||||||
|
it.child.children.last().value
|
||||||
|
} else if it.func() == sequence and it.children.len() >= 2 {
|
||||||
|
let last = it.children.last()
|
||||||
|
if (
|
||||||
|
last.at("label", default: none) == lbl-tag
|
||||||
|
// Workaround for 0.11.0 weirdly placing some 'meta' element sometimes
|
||||||
|
or sys.version < version(0, 12, 0) and {
|
||||||
|
last = it.children.at(it.children.len() - 2)
|
||||||
|
last.at("label", default: none) == lbl-tag
|
||||||
|
}
|
||||||
|
// Pre-1.0 elembic versions didn't add a label to metadata
|
||||||
|
or str(it.at("label", default: "")).starts-with(lbl-show-head) and last.func() == metadata
|
||||||
|
) {
|
||||||
|
// Decomposing a recently-constructed (but not placed) element
|
||||||
|
last.value
|
||||||
|
} else {
|
||||||
|
(data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false)
|
||||||
|
}
|
||||||
|
} else if it.func() == figure and it.has("kind") and it.kind == labelable-elem-figure-kind {
|
||||||
|
let last
|
||||||
|
if it.body.func() == sequence and it.body.children.len() >= 2 and {
|
||||||
|
last = it.body.children.last()
|
||||||
|
(
|
||||||
|
last.at("label", default: none) == lbl-tag
|
||||||
|
// Workaround for 0.11.0 weirdly placing some 'meta' element sometimes
|
||||||
|
or sys.version < version(0, 12, 0) and {
|
||||||
|
last = it.children.at(it.children.len() - 2)
|
||||||
|
last.at("label", default: none) == lbl-tag
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} {
|
||||||
|
last.value
|
||||||
|
} else {
|
||||||
|
(data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false)
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
it.has("label")
|
||||||
|
and str(it.label).starts-with(lbl-outer-head)
|
||||||
|
) {
|
||||||
|
(data-kind: "incomplete-element-instance", body: it, fields: (:), func: (:), eid: str(it.label).slice(lbl-outer-head.len()), fields-known: false, valid: false)
|
||||||
|
} else {
|
||||||
|
(data-kind: "content", body: it, fields: it.fields(), func: it.func(), eid: none, fields-known: false, valid: false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the fields of a type instance or element instance (custom or not).
|
||||||
|
//
|
||||||
|
// SAMPLE USAGE:
|
||||||
|
//
|
||||||
|
// #show e.selector(elem): it => {
|
||||||
|
// let fields = e.fields(it)
|
||||||
|
// [Hello #fields.name!]
|
||||||
|
// }
|
||||||
|
#let fields(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
|
||||||
|
if type(info) == dictionary and "data-kind" in info {
|
||||||
|
if info.data-kind in ("content", "element-instance", "type-instance") {
|
||||||
|
return info.fields
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(:)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain context at an element's site.
|
||||||
|
//
|
||||||
|
// SAMPLE USAGE:
|
||||||
|
//
|
||||||
|
// 1. In show rules:
|
||||||
|
//
|
||||||
|
// #show e.selector(elem): it => {
|
||||||
|
// let (get, ..) = e.ctx(it)
|
||||||
|
// let other-elem-ctx = get(other-elem)
|
||||||
|
// [The other element field was set to #other-elem-ctx.field at that point!]
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// 2. In element declarations:
|
||||||
|
//
|
||||||
|
// #e.element.declare(
|
||||||
|
// ...
|
||||||
|
// synthesize: it => {
|
||||||
|
// // Get context for other element
|
||||||
|
// it.some-field = (e.ctx(it).get)(other-elem).field
|
||||||
|
// },
|
||||||
|
// ...
|
||||||
|
// )
|
||||||
|
#let ctx(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
if type(info) == dictionary and "ctx" in info {
|
||||||
|
info.ctx
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain an element's or type's scope (usually a module with important definitions).
|
||||||
|
//
|
||||||
|
// SAMPLE USAGE:
|
||||||
|
//
|
||||||
|
// #let subelem = e.scope(elem).subelem
|
||||||
|
#let scope(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
if type(info) == dictionary and "scope" in info {
|
||||||
|
info.scope
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain an element's or custom type's constructor function.
|
||||||
|
/// For native elements, this will be equivalent to `it.func()`.
|
||||||
|
///
|
||||||
|
/// Useful in custom element show rules, for example.
|
||||||
|
///
|
||||||
|
/// This is equivalent to `e.data(it).func`.
|
||||||
|
///
|
||||||
|
/// SAMPLE USAGE:
|
||||||
|
///
|
||||||
|
/// ```typ
|
||||||
|
/// #show selector.or(e.selector(elem1), e.selector(elem2)): it => {
|
||||||
|
/// // Will be either elem1 or elem2
|
||||||
|
/// let elem = e.func(it)
|
||||||
|
/// // 'elem == elem1' works, but comparing 'eid's is recommended
|
||||||
|
/// if e.eid(elem) == e.eid(elem1) {
|
||||||
|
/// [This is elem1]
|
||||||
|
/// } else {
|
||||||
|
/// [This is elem2]
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - it (any): element/custom type instance (or element/custom type itself) to get the constructor from
|
||||||
|
/// -> function | none
|
||||||
|
#let func(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
if type(info) == dictionary and "func" in info {
|
||||||
|
info.func
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain an element's eid. It uniquely distinguishes this element from others,
|
||||||
|
/// even if they have the same name, by including both its prefix and name.
|
||||||
|
///
|
||||||
|
/// This is equivalent to `e.data(elem).eid`.
|
||||||
|
///
|
||||||
|
/// - elem (any): custom element (or an instance of it) to get 'eid' from
|
||||||
|
/// -> function | none
|
||||||
|
#let eid(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
if type(info) == dictionary and "eid" in info {
|
||||||
|
info.eid
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Obtain a custom type's tid. It uniquely distinguishes a custom type from
|
||||||
|
/// others, even if they have the same name, by including both its prefix and
|
||||||
|
/// name. Returns `none` on invalid input.
|
||||||
|
///
|
||||||
|
/// This is equivalent to `e.data(typ).tid`.
|
||||||
|
///
|
||||||
|
/// - typ (any): custom type (or an instance of it) to get 'tid' from
|
||||||
|
/// -> function | none
|
||||||
|
#let tid(it) = {
|
||||||
|
let info = data(it)
|
||||||
|
if type(info) == dictionary and "tid" in info {
|
||||||
|
info.tid
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain an element's counter.
|
||||||
|
//
|
||||||
|
// USAGE:
|
||||||
|
//
|
||||||
|
// #context {
|
||||||
|
// [The element counter value is #e.counter(elem).get().first()]
|
||||||
|
// }
|
||||||
|
#let counter_(elem) = {
|
||||||
|
let info = data(elem)
|
||||||
|
|
||||||
|
if type(info) == dictionary and "data-kind" in info and (info.data-kind == "element" or info.data-kind == "element-instance") {
|
||||||
|
info.counter
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: e.counter: this is not an element")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the name of a content's constructor function as a string.
|
||||||
|
///
|
||||||
|
/// Returns 'none' on invalid input.
|
||||||
|
///
|
||||||
|
/// USAGE:
|
||||||
|
///
|
||||||
|
/// ```typ
|
||||||
|
/// assert.eq(func-name(my-elem()), "my-elem")
|
||||||
|
/// assert.eq(func-name([= abc]), "heading")
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - c (content): content to get the constructor function of
|
||||||
|
/// -> function | none
|
||||||
|
#let func-name(c) = {
|
||||||
|
if type(c) == function {
|
||||||
|
let func-data = data(c)
|
||||||
|
return if "name" in func-data {
|
||||||
|
func-data.name
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
} else if type(c) != content {
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
let name = repr(c.func())
|
||||||
|
if c.func() in elem-funcs {
|
||||||
|
let element-data = data(c)
|
||||||
|
if "eid" in element-data and element-data.eid != none {
|
||||||
|
name = if "name" in element-data and type(element-data.name) == str { element-data.name } else { "unknown custom element" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
name
|
||||||
|
}
|
||||||
|
|
||||||
|
#let _letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-"
|
||||||
|
|
||||||
|
/// This is used to obtain a debug representation of custom elements and types.
|
||||||
|
///
|
||||||
|
/// Also supports native types (just calls `repr()` for them).
|
||||||
|
///
|
||||||
|
/// - value (any): value to represent
|
||||||
|
/// - depth (int): current depth (must start at 0, conservative limit of 10 for now)
|
||||||
|
/// -> str
|
||||||
|
#let repr_(value, depth: 0) = {
|
||||||
|
if depth >= 10 {
|
||||||
|
return repr(value)
|
||||||
|
}
|
||||||
|
let typename = ""
|
||||||
|
let value-type = type(value)
|
||||||
|
if value-type == content and value.func() in elem-funcs {
|
||||||
|
let value-data = data(value)
|
||||||
|
if "eid" in value-data and value-data.eid != none {
|
||||||
|
value = value-data.fields
|
||||||
|
value-type = dictionary
|
||||||
|
typename = if "name" in value-data and type(value-data.name) == str {
|
||||||
|
value-data.name
|
||||||
|
} else {
|
||||||
|
"unknown-element"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if value-type == dictionary {
|
||||||
|
let pairs = if typename != "" {
|
||||||
|
// Element fields => sort
|
||||||
|
value.pairs().sorted(key: ((k, _)) => k)
|
||||||
|
} else if custom-type-key in value {
|
||||||
|
let type-data = value.at(custom-type-key)
|
||||||
|
|
||||||
|
let id = type-data.id
|
||||||
|
|
||||||
|
typename = if "name" in id {
|
||||||
|
id.name
|
||||||
|
} else if id == "custom type" {
|
||||||
|
return if custom-type-data-key in value {
|
||||||
|
"custom-type(name: " + repr(value.name) + ", tid: " + repr(value.tid) + ")"
|
||||||
|
} else {
|
||||||
|
"custom-type()"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
type-data.fields.pairs().sorted(key: ((k, _)) => k)
|
||||||
|
} else {
|
||||||
|
value.pairs()
|
||||||
|
}
|
||||||
|
|
||||||
|
typename
|
||||||
|
"("
|
||||||
|
pairs.map(((k, v)) => {
|
||||||
|
if k.codepoints().all(c => c in _letters) {
|
||||||
|
k
|
||||||
|
} else {
|
||||||
|
repr(k)
|
||||||
|
}
|
||||||
|
|
||||||
|
": "
|
||||||
|
|
||||||
|
repr_(v, depth: depth + 1)
|
||||||
|
}).join(", ")
|
||||||
|
")"
|
||||||
|
} else if value-type == array {
|
||||||
|
"("
|
||||||
|
value.map(repr_.with(depth: depth + 1)).join(", ")
|
||||||
|
")"
|
||||||
|
} else {
|
||||||
|
repr(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Performs deep equality of values.
|
||||||
|
///
|
||||||
|
/// This is necessary to reliably compare instances of the same element or
|
||||||
|
/// custom type, as well as data structures containing them such as arrays
|
||||||
|
/// or dictionary, between different versions of the same element or type,
|
||||||
|
/// by recursively comparing `eid(a) == eid(b) and fields(a) == fields(b)`.
|
||||||
|
/// However, this is notably slower than Typst's built-in equality check.
|
||||||
|
///
|
||||||
|
/// - a (any): First value to compare.
|
||||||
|
/// - b (any): Second value to compare.
|
||||||
|
/// -> bool
|
||||||
|
#let eq(a, b) = {
|
||||||
|
if a == b {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if type(a) != type(b) or type(a) not in (content, dictionary, array) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recursively compare until we find a 'false'
|
||||||
|
let stack = ((a, b),)
|
||||||
|
let fuel = 3000
|
||||||
|
while stack != () {
|
||||||
|
let (a, b) = stack.pop()
|
||||||
|
if a == b {
|
||||||
|
// Good!
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fuel -= 1
|
||||||
|
if fuel == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
// Of course, the types must match
|
||||||
|
let a-type = type(a)
|
||||||
|
let b-type = type(b)
|
||||||
|
if a-type != b-type {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if a-type == array {
|
||||||
|
if a.len() != b.len() {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
stack += array.zip(a, b)
|
||||||
|
|
||||||
|
// Only have special checks for composed types and custom types and elements
|
||||||
|
// of same type
|
||||||
|
} else if (a-type == content or a-type == dictionary) and eid(a) == eid(b) and tid(a) == tid(b) {
|
||||||
|
if eid(a) != none or tid(a) != none or a-type == content and a.func() == b.func() {
|
||||||
|
// Same element id, compare their fields
|
||||||
|
a = fields(a)
|
||||||
|
b = fields(b)
|
||||||
|
a-type = type(a)
|
||||||
|
if a-type != type(b) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if a-type != dictionary or a.len() != b.len() {
|
||||||
|
// Fields were invalid, or content didn't have the same func
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for (key, a-val) in a {
|
||||||
|
if key not in b {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
stack.push((a-val, b.at(key),))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No checks failed
|
||||||
|
true
|
||||||
|
}
|
||||||
3693
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/element.typ
vendored
Normal file
3693
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/element.typ
vendored
Normal file
File diff suppressed because it is too large
Load Diff
366
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/fields.typ
vendored
Normal file
366
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/fields.typ
vendored
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
#import "data.typ": type-key, custom-type-key, current-field-version, eq
|
||||||
|
#import "types/types.typ"
|
||||||
|
|
||||||
|
#let field-key = "__elembic_field"
|
||||||
|
#let fields-key = "__elembic_fields"
|
||||||
|
|
||||||
|
#let _missing() = {}
|
||||||
|
|
||||||
|
// Specifies an element field's properties.
|
||||||
|
#let field(
|
||||||
|
name,
|
||||||
|
type_,
|
||||||
|
doc: none,
|
||||||
|
required: false,
|
||||||
|
named: auto,
|
||||||
|
synthesized: false,
|
||||||
|
default: _missing,
|
||||||
|
folds: auto,
|
||||||
|
internal: false,
|
||||||
|
meta: (:),
|
||||||
|
) = {
|
||||||
|
assert(type(name) == str, message: "elembic: field: Field name must be a string, not " + str(type(name)))
|
||||||
|
|
||||||
|
let error-prefix = "elembic: field '" + name + "': "
|
||||||
|
assert(doc == none or type(doc) == str, message: error-prefix + "'doc' must be none or a string (add documentation)")
|
||||||
|
assert(type(synthesized) == bool, message: error-prefix + "'synthesized' must be a boolean (true: field is automatically synthesized and cannot be specified or overridden by the user; false: field can be manually specified and overridden by the user)")
|
||||||
|
assert(type(required) == bool, message: error-prefix + "'required' must be a boolean")
|
||||||
|
assert(folds == auto or type(folds) == bool, message: error-prefix + "'folds' must be a boolean or auto")
|
||||||
|
assert(folds != true or not required, message: error-prefix + "'folds' cannot be set to 'true' on required fields at the moment")
|
||||||
|
assert(type(internal) == bool, message: error-prefix + "'internal' must be a boolean")
|
||||||
|
assert(type(meta) == dictionary, message: error-prefix + "'meta' must be a dictionary")
|
||||||
|
assert(named == auto or type(named) == bool, message: error-prefix + "'named' must be a boolean or auto")
|
||||||
|
let typeinfo = {
|
||||||
|
let (res, value) = types.validate(type_)
|
||||||
|
assert(res, message: if not res { error-prefix + value } else { "" })
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
if not required and default == _missing {
|
||||||
|
let (res, value) = types.default(typeinfo)
|
||||||
|
assert(res, message: if not res { error-prefix + value } else { "" })
|
||||||
|
|
||||||
|
default = value
|
||||||
|
}
|
||||||
|
|
||||||
|
default = if required or synthesized {
|
||||||
|
// This value should be ignored in that case
|
||||||
|
auto
|
||||||
|
} else {
|
||||||
|
let (success, value) = types.cast(default, typeinfo)
|
||||||
|
if not success {
|
||||||
|
assert(false, message: error-prefix + value + "\n hint: given default for field had an incompatible type")
|
||||||
|
}
|
||||||
|
|
||||||
|
value
|
||||||
|
}
|
||||||
|
|
||||||
|
if folds == auto {
|
||||||
|
folds = not required
|
||||||
|
}
|
||||||
|
|
||||||
|
let fold = if folds and not synthesized and "fold" in typeinfo and typeinfo.fold != none {
|
||||||
|
assert(typeinfo.fold == auto or type(typeinfo.fold) == function, message: error-prefix + "type '" + typeinfo.name + "' doesn't appear to have a valid fold field (must be auto or function)")
|
||||||
|
let fold-default = if required {
|
||||||
|
// No field default
|
||||||
|
assert(false, message: "elembic: internal error: required field must not be foldable")
|
||||||
|
} else {
|
||||||
|
// Use the field default as starting point for folding
|
||||||
|
default
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
folder: typeinfo.fold,
|
||||||
|
default: fold-default,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
|
||||||
|
if named == auto {
|
||||||
|
// Pos arg is generally required
|
||||||
|
named = not required
|
||||||
|
}
|
||||||
|
|
||||||
|
if synthesized and (required or not named) {
|
||||||
|
assert(false, message: error-prefix + "synthesized field cannot be required or positional, since it cannot be specified by the user")
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
(field-key): true,
|
||||||
|
version: current-field-version,
|
||||||
|
name: name,
|
||||||
|
doc: doc,
|
||||||
|
typeinfo: typeinfo,
|
||||||
|
default: default,
|
||||||
|
required: required,
|
||||||
|
synthesized: synthesized,
|
||||||
|
named: named,
|
||||||
|
fold: fold,
|
||||||
|
folds: folds,
|
||||||
|
internal: internal,
|
||||||
|
meta: meta,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let parse-fields(fields, allow-unknown-fields: false) = {
|
||||||
|
assert(type(allow-unknown-fields) == bool, message: "elembic: element.fields: 'allow-unknown-fields' must be a boolean, not " + str(type(allow-unknown-fields)))
|
||||||
|
|
||||||
|
let required-pos-fields = ()
|
||||||
|
let optional-pos-fields = ()
|
||||||
|
let required-named-fields = ()
|
||||||
|
let optional-named-fields = ()
|
||||||
|
let all-fields = (:)
|
||||||
|
let user-named-fields = (:)
|
||||||
|
let foldable-fields = (:)
|
||||||
|
let user-fields = (:)
|
||||||
|
let synthesized-fields = (:)
|
||||||
|
|
||||||
|
for field in fields {
|
||||||
|
assert(type(field) == dictionary and field.at(field-key, default: none) == true, message: "elembic: element.fields: Invalid field received, please use the 'e.fields.field' constructor.")
|
||||||
|
assert(field.named or not field.required or optional-pos-fields == (), message: "elembic: element.fields: field '" + field.name + "' cannot be positional and required and appear after other positional but optional fields. Ensure there are only optional fields after the first positional optional field.")
|
||||||
|
assert(field.name not in all-fields, message: "elembic: element.fields: duplicate field name '" + field.name + "'")
|
||||||
|
|
||||||
|
if field.required {
|
||||||
|
if field.named {
|
||||||
|
required-named-fields.push(field)
|
||||||
|
} else {
|
||||||
|
required-pos-fields.push(field)
|
||||||
|
}
|
||||||
|
} else if field.named {
|
||||||
|
optional-named-fields.push(field)
|
||||||
|
} else {
|
||||||
|
optional-pos-fields.push(field)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.fold != none {
|
||||||
|
foldable-fields.insert(field.name, field.fold)
|
||||||
|
}
|
||||||
|
|
||||||
|
if field.synthesized {
|
||||||
|
synthesized-fields.insert(field.name, field)
|
||||||
|
} else {
|
||||||
|
user-fields.insert(field.name, field)
|
||||||
|
|
||||||
|
if field.named {
|
||||||
|
user-named-fields.insert(field.name, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
all-fields.insert(field.name, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
(fields-key): true,
|
||||||
|
version: current-field-version,
|
||||||
|
required-pos-fields: required-pos-fields,
|
||||||
|
optional-pos-fields: optional-pos-fields,
|
||||||
|
required-named-fields: required-named-fields,
|
||||||
|
optional-named-fields: optional-named-fields,
|
||||||
|
foldable-fields: foldable-fields,
|
||||||
|
user-named-fields: user-named-fields,
|
||||||
|
user-fields: user-fields,
|
||||||
|
all-fields: all-fields,
|
||||||
|
allow-unknown-fields: allow-unknown-fields,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates an argument parser function with the given general error
|
||||||
|
// prefix (for listing missing fields) and per-field error prefix function
|
||||||
|
// (for an invalid field; receives the field name).
|
||||||
|
//
|
||||||
|
// You can customize 'field-term' to customize what the word "field" is
|
||||||
|
// in error messages. It should be either a string or a two-element
|
||||||
|
// array with (singular, plural). Setting 'typecheck: false' also fully
|
||||||
|
// disables typechecking.
|
||||||
|
//
|
||||||
|
// Parse arguments into a dictionary of fields and their casted values.
|
||||||
|
// By default, include required arguments and error if they are missing.
|
||||||
|
// Setting 'include-required' to false will error if they are present
|
||||||
|
// instead.
|
||||||
|
#let generate-arg-parser(
|
||||||
|
fields: none,
|
||||||
|
general-error-prefix: "",
|
||||||
|
field-error-prefix: _ => "",
|
||||||
|
field-term: "field",
|
||||||
|
typecheck: true,
|
||||||
|
) = {
|
||||||
|
assert(type(fields) == dictionary and fields-key in fields, message: "elembic: generate-arg-parser: please use 'parse-fields' to generate the fields input.")
|
||||||
|
assert(type(general-error-prefix) == str, message: "elembic: generate-arg-parser: 'general-error-prefix' must be a string")
|
||||||
|
assert(type(field-error-prefix) == function, message: "elembic: generate-arg-parser: 'field-error-prefix' must be a function receiving field name and returning string")
|
||||||
|
assert(type(typecheck) == bool, message: "elembic: generate-arg-parser: 'typecheck' must be a boolean, not " + str(type(typecheck)))
|
||||||
|
|
||||||
|
let (field-singular, field-plural) = if type(field-term) == str {
|
||||||
|
(field-term, field-term + "s")
|
||||||
|
} else if type(field-term) == array and field-term.len() == 2 and field-term.all(term => type(term) == str) {
|
||||||
|
field-term
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: generate-arg-parser: 'field-term' must either be a string (plural with 's') or a two-element array of strings (singular, plural).")
|
||||||
|
}
|
||||||
|
|
||||||
|
let (required-pos-fields, optional-pos-fields, required-named-fields, optional-named-fields, all-fields, user-fields, user-named-fields, allow-unknown-fields) = fields
|
||||||
|
let required-pos-fields-amount = required-pos-fields.len()
|
||||||
|
let optional-pos-fields-amount = optional-pos-fields.len()
|
||||||
|
let total-pos-fields-amount = required-pos-fields-amount + optional-pos-fields-amount
|
||||||
|
let all-pos-fields = required-pos-fields + optional-pos-fields
|
||||||
|
|
||||||
|
let has-required-fields = required-pos-fields-amount + required-named-fields.len() != 0
|
||||||
|
|
||||||
|
// If we allow unknown named fields, we still need to check whether a
|
||||||
|
// positional or synthesized field was accidentally specified as a named field.
|
||||||
|
let is-unknown-named-field = if allow-unknown-fields {
|
||||||
|
f => f in all-fields and f not in user-named-fields
|
||||||
|
} else {
|
||||||
|
f => f not in user-named-fields
|
||||||
|
}
|
||||||
|
|
||||||
|
// Disable typechecking anyway if all fields are 'any'
|
||||||
|
//
|
||||||
|
// Have a separate typecheck option so type information can be kept in fields
|
||||||
|
// even if typechecking is undesirable
|
||||||
|
// Note: we don't parse args for synthesized fields, so we can exclude them when
|
||||||
|
// checking whether we will typecheck when parsing args
|
||||||
|
let typecheck = typecheck and user-fields.values().any(f => f.typeinfo.type-kind != "any")
|
||||||
|
|
||||||
|
// Parse args (no typechecking)
|
||||||
|
let parse-args-no-typechecking(args, include-required: true) = {
|
||||||
|
let pos = args.pos()
|
||||||
|
|
||||||
|
if include-required and pos.len() < required-pos-fields-amount {
|
||||||
|
// Plural
|
||||||
|
let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if pos.len() > if include-required { total-pos-fields-amount } else { optional-pos-fields-amount } {
|
||||||
|
let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount }
|
||||||
|
let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" }
|
||||||
|
return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
let named-args = args.named()
|
||||||
|
if include-required {
|
||||||
|
if required-named-fields.any(f => f.name not in named-args) {
|
||||||
|
let missing-fields = required-named-fields.filter(f => f.name not in named-args)
|
||||||
|
let term = if missing-fields.len() == 1 { field-singular } else { field-plural }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", "))
|
||||||
|
}
|
||||||
|
} else if required-named-fields.any(f => f.name in named-args) {
|
||||||
|
let field = required-named-fields.find(f => f.name in named-args)
|
||||||
|
return (false, field-error-prefix(field.name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Here we simultaneously check for unknown fields and for positional fields
|
||||||
|
// being wrongly specified as named. If there are no positional fields and
|
||||||
|
// unknown fields are allowed, there is no point in doing this check.
|
||||||
|
if (not allow-unknown-fields or total-pos-fields-amount > 0) and named-args.keys().any(is-unknown-named-field) {
|
||||||
|
let field-name = named-args.keys().find(is-unknown-named-field)
|
||||||
|
let field = all-fields.at(field-name, default: none)
|
||||||
|
let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" }
|
||||||
|
let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields }
|
||||||
|
let i = 0
|
||||||
|
for value in pos {
|
||||||
|
let pos-field = pos-fields.at(i)
|
||||||
|
named-args.insert(pos-field.name, value)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
(true, named-args)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse args (with typechecking)
|
||||||
|
let parse-args(args, include-required: true) = {
|
||||||
|
let result = (:)
|
||||||
|
|
||||||
|
let pos = args.pos()
|
||||||
|
if include-required and pos.len() < required-pos-fields-amount {
|
||||||
|
// Plural
|
||||||
|
let term = if required-pos-fields-amount - pos.len() == 1 { field-singular } else { field-plural }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "missing positional " + term + " " + fields.required-pos-fields.slice(pos.len()).map(f => "'" + f.name + "'").join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected-arg-amount = if include-required { total-pos-fields-amount } else { optional-pos-fields-amount }
|
||||||
|
|
||||||
|
if pos.len() > expected-arg-amount {
|
||||||
|
let excluding-required-hint = if include-required { "" } else { "\n hint: only optional fields are accepted here" }
|
||||||
|
return (false, general-error-prefix + "too many positional arguments, expected " + str(expected-arg-amount) + excluding-required-hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
let named-args = args.named()
|
||||||
|
|
||||||
|
if include-required and required-named-fields.any(f => f.name not in named-args) {
|
||||||
|
let missing-fields = required-named-fields.filter(f => f.name not in named-args)
|
||||||
|
let term = if missing-fields.len() == 1 { field-singular } else { field-plural }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "missing required named " + term + " " + missing-fields.map(f => "'" + f.name + "'").join(", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
for (field-name, value) in named-args {
|
||||||
|
if allow-unknown-fields and field-name not in all-fields {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
let field = all-fields.at(field-name, default: none)
|
||||||
|
|
||||||
|
if field == none or field.synthesized or not field.named {
|
||||||
|
let expected-pos-hint = if field == none or field.named { "" } else { "\n hint: this " + field-singular + " must be specified positionally" }
|
||||||
|
let is-synthesized-hint = if field != none and field.synthesized { "\n hint: this " + field-singular + " is synthesized and cannot be specified manually" } else { "" }
|
||||||
|
|
||||||
|
return (false, general-error-prefix + "unknown named " + field-singular + " '" + field-name + "'" + expected-pos-hint + is-synthesized-hint)
|
||||||
|
}
|
||||||
|
|
||||||
|
if not include-required and field.required {
|
||||||
|
return (false, field-error-prefix(field-name) + "this " + field-singular + " cannot be specified here\n hint: only optional " + field-plural + " are accepted here")
|
||||||
|
}
|
||||||
|
|
||||||
|
let typeinfo = field.typeinfo
|
||||||
|
let kind = typeinfo.type-kind
|
||||||
|
|
||||||
|
if kind != "any" {
|
||||||
|
let (res, casted) = types.cast(value, typeinfo)
|
||||||
|
if not res {
|
||||||
|
return (false, field-error-prefix(field-name) + casted)
|
||||||
|
}
|
||||||
|
named-args.insert(field-name, casted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos-fields = if include-required { all-pos-fields } else { optional-pos-fields }
|
||||||
|
let i = 0
|
||||||
|
for value in pos {
|
||||||
|
let pos-field = pos-fields.at(i)
|
||||||
|
let typeinfo = pos-field.typeinfo
|
||||||
|
let kind = typeinfo.type-kind
|
||||||
|
let casted = value
|
||||||
|
|
||||||
|
if kind != "any" {
|
||||||
|
let res
|
||||||
|
(res, casted) = types.cast(value, typeinfo)
|
||||||
|
if not res {
|
||||||
|
return (false, field-error-prefix(pos-field.name) + casted)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
named-args.insert(pos-field.name, casted)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
(true, named-args)
|
||||||
|
}
|
||||||
|
|
||||||
|
if typecheck {
|
||||||
|
parse-args
|
||||||
|
} else {
|
||||||
|
parse-args-no-typechecking
|
||||||
|
}
|
||||||
|
}
|
||||||
430
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/filters.typ
vendored
Normal file
430
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/filters.typ
vendored
Normal file
@@ -0,0 +1,430 @@
|
|||||||
|
#import "data.typ": element-version, filter-key, special-data-values
|
||||||
|
|
||||||
|
// Check if an element instance satisfies a filter.
|
||||||
|
//
|
||||||
|
// Assumes this filter already accepts this element, so eid is not checked.
|
||||||
|
#let verify-filter(fields, eid: none, filter: none, ancestry: ()) = {
|
||||||
|
if filter == none {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if eid == none {
|
||||||
|
assert(false, message: "elembic: element.verify-filter: eid must not be none")
|
||||||
|
}
|
||||||
|
|
||||||
|
if "__future" in filter and element-version <= filter.__future.max-version {
|
||||||
|
return (filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version)
|
||||||
|
} else if filter.kind == "where" {
|
||||||
|
return eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v)
|
||||||
|
} else if filter.kind == "where-any" {
|
||||||
|
return eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v))
|
||||||
|
} else if filter.kind == "custom" {
|
||||||
|
return (filter.elements == none or eid in filter.elements) and (filter.call)(
|
||||||
|
fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manually simulate a recursive algorithm.
|
||||||
|
// Normally, for OR(A, B), we could just call (verify(A), verify(B)), but
|
||||||
|
// recursive calls are limited and expensive.
|
||||||
|
// We instead do the following:
|
||||||
|
// - Have a stack of filters pending evaluation.
|
||||||
|
// - Have a stack of evaluation results (operands). This is only used for
|
||||||
|
// non-short circuiting operations (see below).
|
||||||
|
// - Each time a filter is pushed to the pending stack, we push its operands
|
||||||
|
// to the pending stack too, until the top of the stack has no further
|
||||||
|
// operands, and mark the filter as "visited" so we don't add its operands
|
||||||
|
// again. Note that operands are always pushed in reverse for short circuit
|
||||||
|
// to work, since we have to evaluate - thus pop from the end - each operand
|
||||||
|
// in its original order.
|
||||||
|
// - We evaluate each leaf filter (where or custom) and push their results to
|
||||||
|
// 'operands' (in reverse order, from last to first).
|
||||||
|
// - We then reach the filter that will use the latest N results from
|
||||||
|
// 'operands' and push that filter's evaluated result (e.g. AND of the latest
|
||||||
|
// two results) into 'operands'.
|
||||||
|
// - Repeat the process until the filter stack is empty (all evaluated) and
|
||||||
|
// operands has only a single element (for the root filter).
|
||||||
|
// - If operands is empty or has more than one element, something bad
|
||||||
|
// happened. Otherwise, its only remaining element is the evaluated result of
|
||||||
|
// the root filter.
|
||||||
|
//
|
||||||
|
// We also have an "op-stack" to indicate the latest operation whose operands
|
||||||
|
// were expanded into the filter stack.
|
||||||
|
//
|
||||||
|
// The idea is to allow short circuiting when the latest operation is an AND
|
||||||
|
// or OR. Otherwise, the operation in op-stack is only used to indicate the
|
||||||
|
// latest operation doesn't short-circuit.
|
||||||
|
//
|
||||||
|
// It works as follows: we store the filter stack state in "op-stack"
|
||||||
|
// whenever we push an operation, such as and, or, xor etc. When the latest
|
||||||
|
// pushed operation is an "and" and the current filter returned false, we
|
||||||
|
// immediately restore the filter state at the "and" (ignore its other
|
||||||
|
// operands) and assign its value to "false". If it was an "or", we do the
|
||||||
|
// same if the current filter returned true, assigning its value to true.
|
||||||
|
let filter-stack = (filter,)
|
||||||
|
let op-stack = ()
|
||||||
|
let operands = ()
|
||||||
|
while filter-stack != () {
|
||||||
|
let last = filter-stack.last()
|
||||||
|
|
||||||
|
// Expand the latest filter's children into the evaluation stack.
|
||||||
|
while (
|
||||||
|
last.at(filter-key) != "visited"
|
||||||
|
and ("__future" not in last or element-version > last.__future.max-version)
|
||||||
|
and "operands" in last
|
||||||
|
and last.operands != ()
|
||||||
|
) {
|
||||||
|
// Ensure we don't reach the parent operation until we have evaluated
|
||||||
|
// each child operation.
|
||||||
|
op-stack.push((last.kind, filter-stack.len() - 1))
|
||||||
|
filter-stack.last().at(filter-key) = "visited"
|
||||||
|
if "__subject" in filter {
|
||||||
|
// Ensure children filters apply to the same subject.
|
||||||
|
filter-stack += last.operands.map(op => if "__subject" in op { op } else { (..op, __subject: filter.__subject) }).rev()
|
||||||
|
} else {
|
||||||
|
// In reverse order to pop the first operand first.
|
||||||
|
filter-stack += last.operands.rev()
|
||||||
|
}
|
||||||
|
last = filter-stack.last()
|
||||||
|
}
|
||||||
|
|
||||||
|
let filter = filter-stack.pop()
|
||||||
|
let (kind,) = filter
|
||||||
|
let fields = fields
|
||||||
|
let eid = eid
|
||||||
|
let ancestry = ancestry
|
||||||
|
if "__subject" in filter {
|
||||||
|
(fields, eid, ancestry) = filter.__subject
|
||||||
|
}
|
||||||
|
|
||||||
|
let value = if "__future" in filter and element-version <= filter.__future.max-version {
|
||||||
|
(filter.__future.call)(fields, eid: eid, filter: filter, ancestry: ancestry, __future-version: element-version)
|
||||||
|
} else if kind == "where" {
|
||||||
|
eid == filter.eid and filter.fields.pairs().all(((k, v)) => k in fields and fields.at(k) == v)
|
||||||
|
} else if kind == "where-any" {
|
||||||
|
eid in filter.fields-any and filter.fields-any.at(eid).any(f => f.pairs().all(((k, v)) => k in fields and fields.at(k) == v))
|
||||||
|
} else if kind == "custom" {
|
||||||
|
(filter.elements == none or eid in filter.elements) and (filter.call)(
|
||||||
|
fields, eid: eid, ancestry: if filter.may-need-ancestry { ancestry } else { () }, __please-use-var-args: true
|
||||||
|
)
|
||||||
|
} else if kind == "within" {
|
||||||
|
// Expand 'within' filter into
|
||||||
|
// (ancestor 1 matches OR ancestor 2 matches OR ...)
|
||||||
|
if filter.elements == none or eid in filter.elements {
|
||||||
|
let (ancestor-filter,) = filter
|
||||||
|
let matching-ancestors = if "depth" in filter and filter.depth != none and filter.depth > 0 {
|
||||||
|
let total-depth = ancestry.len()
|
||||||
|
if total-depth >= filter.depth {
|
||||||
|
((total-depth - filter.depth, ancestry.at(total-depth - filter.depth)),)
|
||||||
|
} else {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
} else if "max-depth" in filter and filter.max-depth != none and filter.max-depth > 0 {
|
||||||
|
let total-depth = ancestry.len()
|
||||||
|
if total-depth <= filter.max-depth {
|
||||||
|
ancestry.enumerate()
|
||||||
|
} else {
|
||||||
|
ancestry.enumerate().slice(total-depth - filter.max-depth)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ancestry.enumerate()
|
||||||
|
}
|
||||||
|
|
||||||
|
filter-stack.push(
|
||||||
|
(
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: "or",
|
||||||
|
operands: matching-ancestors.map(((i, ancestor)) => (
|
||||||
|
..ancestor-filter,
|
||||||
|
|
||||||
|
// TODO: maybe don't clone the ancestry for each ancestor...
|
||||||
|
__subject: (eid: ancestor.eid, fields: ancestor.fields, ancestry: ancestry.slice(0, i))
|
||||||
|
)),
|
||||||
|
elements: ancestor-filter.elements,
|
||||||
|
|
||||||
|
// Since this is an internal filter, doesn't matter
|
||||||
|
ancestry-elements: (:),
|
||||||
|
may-need-ancestry: true,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// This filter won't be evaluated, but rather the pushed OR.
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invalid
|
||||||
|
false
|
||||||
|
} else if kind == "and" {
|
||||||
|
// Due to short-circuiting, a false would have failed earlier.
|
||||||
|
true
|
||||||
|
} else if kind == "or" {
|
||||||
|
// Due to short-circuiting, a true would have succeeded earlier.
|
||||||
|
false
|
||||||
|
} else if "operands" in filter {
|
||||||
|
let first-applied-operand = operands.len() - filter.operands.len()
|
||||||
|
// Operation requires N operands => take N operands from the top of the
|
||||||
|
// stack.
|
||||||
|
let applied-operands = operands.slice(first-applied-operand)
|
||||||
|
operands = operands.slice(0, first-applied-operand)
|
||||||
|
|
||||||
|
if kind == "not" {
|
||||||
|
assert(applied-operands.len() == 1, message: "elembic: element.verify-filter: expected one child filter for 'not'")
|
||||||
|
(filter.elements == none or eid in filter.elements) and not applied-operands.first()
|
||||||
|
} else if kind == "xor" {
|
||||||
|
assert(applied-operands.len() == 2, message: "elembic: element.verify-filter: expected two children filters for 'xor'")
|
||||||
|
// Here the order doesn't matter, since we always need to evaluate both
|
||||||
|
// XOR operands (no short-circuit).
|
||||||
|
applied-operands.first() != applied-operands.at(1)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: element.verify-filter: unsupported filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: element.verify-filter: unsupported or invalid filter kind '" + kind + "'\n\nhint: this might mean you're using packages depending on conflicting elembic versions. Please ensure your dependencies are up-to-date.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if op-stack != () and op-stack.last().at(1) == filter-stack.len() {
|
||||||
|
// We have just evaluated this operation.
|
||||||
|
_ = op-stack.pop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Short-circuit: for certain operations, a specific value must stop all
|
||||||
|
// other operand filters from running.
|
||||||
|
let (current-op, op-pos) = if op-stack == () { (none, none) } else { op-stack.last() }
|
||||||
|
while current-op == "and" and not value or current-op == "or" and value {
|
||||||
|
filter-stack = filter-stack.slice(0, op-pos)
|
||||||
|
_ = op-stack.pop()
|
||||||
|
if op-stack == () {
|
||||||
|
current-op = none
|
||||||
|
op-pos = none
|
||||||
|
break
|
||||||
|
} else {
|
||||||
|
(current-op, op-pos) = op-stack.last()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if current-op not in ("and", "or") {
|
||||||
|
operands.push(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if operands.len() != 1 or op-stack != () {
|
||||||
|
assert(false, message: "elembic: element.verify-filter: filter didn't receive enough operands.")
|
||||||
|
}
|
||||||
|
|
||||||
|
operands.first()
|
||||||
|
}
|
||||||
|
|
||||||
|
#let multi-operand-filter(kind: "", arg-count: none) = (..args) => {
|
||||||
|
assert(args.named() == (:), message: "elembic: filters: invalid named arguments given to '" + kind + "' filter constructor.")
|
||||||
|
let filters = args.pos()
|
||||||
|
if arg-count != none and filters.len() != arg-count {
|
||||||
|
assert(false, message: "elembic: filters: must give exactly " + str(arg-count) + " arguments to a '" + kind + "' filter constructor.")
|
||||||
|
}
|
||||||
|
|
||||||
|
filters = filters.map(filter => {
|
||||||
|
if type(filter) == function {
|
||||||
|
filter = filter(__elembic_data: special-data-values.get-where)
|
||||||
|
}
|
||||||
|
assert(type(filter) == dictionary and filter-key in filter, message: "elembic: filters: invalid filter passed to '" + kind + "' constructor, please use 'custom-element.with(...)' to generate a filter.")
|
||||||
|
|
||||||
|
// Flatten "and", "or"
|
||||||
|
if filter.kind == kind and kind in ("and", "or") {
|
||||||
|
filter.operands
|
||||||
|
} else {
|
||||||
|
(filter,)
|
||||||
|
}
|
||||||
|
}).flatten()
|
||||||
|
|
||||||
|
let elements = none
|
||||||
|
if kind == "and" {
|
||||||
|
// Merge where filters as an optimization
|
||||||
|
let where-fields = (:)
|
||||||
|
let where-eid = none
|
||||||
|
let may-merge-filters = filters != ()
|
||||||
|
|
||||||
|
// Intersect elements.
|
||||||
|
// Start accepting all elements and narrow it down from there.
|
||||||
|
for filter in filters {
|
||||||
|
assert("elements" in filter, message: "elembic: filters.and: this filter operand is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.")
|
||||||
|
if elements == none {
|
||||||
|
elements = filter.elements
|
||||||
|
} else if filter.elements != none {
|
||||||
|
// Cannot add new elements, only remove non-shared elements.
|
||||||
|
for (eid, elem-data) in elements {
|
||||||
|
if eid not in filter.elements {
|
||||||
|
_ = elements.remove(eid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter.kind == "where" and may-merge-filters {
|
||||||
|
if where-eid == none {
|
||||||
|
where-eid = filter.eid
|
||||||
|
} else if where-eid != filter.eid {
|
||||||
|
// More than one element to check will never match.
|
||||||
|
may-merge-filters = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
let (eid, fields) = filter
|
||||||
|
if where-fields == (:) {
|
||||||
|
where-fields = fields
|
||||||
|
} else {
|
||||||
|
for (field, value) in fields {
|
||||||
|
if field in where-fields and value != where-fields.at(field) {
|
||||||
|
// and(elem.with(a: 1), elem.with(a: 2))
|
||||||
|
// impossible to match
|
||||||
|
may-merge-filters = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
where-fields.insert(field, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if may-merge-filters {
|
||||||
|
// Has a custom filter, don't merge
|
||||||
|
may-merge-filters = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if may-merge-filters and where-eid != none {
|
||||||
|
// and(elem, elem.with(a: 0), elem.with(b: 1))
|
||||||
|
return (
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: "where",
|
||||||
|
eid: where-eid,
|
||||||
|
fields: where-fields,
|
||||||
|
elements: ((where-eid): elements.at(where-eid)),
|
||||||
|
ancestry-elements: (:),
|
||||||
|
|
||||||
|
// For optimizations
|
||||||
|
may-need-ancestry: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the filters won't match on the wrong elements.
|
||||||
|
// Workaround for e.filters.and_(custom, element)
|
||||||
|
filters = filters.map(f => f + (elements: elements,))
|
||||||
|
|
||||||
|
elements
|
||||||
|
} else if kind == "or" or kind == "xor" {
|
||||||
|
// Join together.
|
||||||
|
elements = (:)
|
||||||
|
let may-merge-filters = kind == "or" and filters != ()
|
||||||
|
let wheres = (:)
|
||||||
|
|
||||||
|
for filter in filters {
|
||||||
|
if "elements" in filter and filter.elements == none {
|
||||||
|
// OR(Any, ...) is always Any.
|
||||||
|
// For XOR, we still have to check all operands so this would also be
|
||||||
|
// Any.
|
||||||
|
elements = none
|
||||||
|
} else if "elements" not in filter or type(filter.elements) != dictionary {
|
||||||
|
assert(false, message: "elembic: filters: invalid operand filter received by '" + kind + "' filter constructor\n\nhint: this filter was likely constructed with an old elembic version. Please update your packages.")
|
||||||
|
} else if elements != none {
|
||||||
|
elements += filter.elements
|
||||||
|
}
|
||||||
|
|
||||||
|
if may-merge-filters and filter.kind in ("where", "where-any") {
|
||||||
|
for (eid, fields) in if filter.kind == "where-any" { filter.fields-any } else { ((filter.eid): (filter.fields,)) } {
|
||||||
|
if eid in wheres {
|
||||||
|
wheres.at(eid) += fields
|
||||||
|
} else {
|
||||||
|
wheres.insert(eid, fields)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if may-merge-filters {
|
||||||
|
// Not all "where"
|
||||||
|
may-merge-filters = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if may-merge-filters {
|
||||||
|
return (
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: "where-any",
|
||||||
|
fields-any: wheres,
|
||||||
|
elements: elements,
|
||||||
|
ancestry-elements: (:),
|
||||||
|
may-need-ancestry: false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
elements
|
||||||
|
} else if kind == "not" {
|
||||||
|
// No elements for NOT since it is unrestricted.
|
||||||
|
// User will have to restrict it manually.
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: filters: internal error: invalid kind '" + kind + "'")
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: kind,
|
||||||
|
operands: filters,
|
||||||
|
elements: elements,
|
||||||
|
ancestry-elements: (:) + filters.map(f => f.at("ancestry-elements", default: none)).join(),
|
||||||
|
may-need-ancestry: filters.any(f => "may-need-ancestry" in f and f.may-need-ancestry),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let or-filter = multi-operand-filter(kind: "or")
|
||||||
|
#let and-filter = multi-operand-filter(kind: "and")
|
||||||
|
#let not-filter = multi-operand-filter(kind: "not", arg-count: 1)
|
||||||
|
#let xor-filter = multi-operand-filter(kind: "xor", arg-count: 2)
|
||||||
|
#let custom-filter(callback) = {
|
||||||
|
assert(type(callback) == function, message: "elembic: filters.custom: 'callback' for custom filter must be a function (fields, eid: eid, ..) => bool.")
|
||||||
|
|
||||||
|
(
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: "custom",
|
||||||
|
call: callback,
|
||||||
|
elements: none,
|
||||||
|
ancestry-elements: (:),
|
||||||
|
may-need-ancestry: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter that only matches when this element is inside another elembic element.
|
||||||
|
///
|
||||||
|
/// For example:
|
||||||
|
///
|
||||||
|
/// ```typ
|
||||||
|
/// #show: e.show_(e.filters.and(elem, e.filters.within(other-elem)), none)
|
||||||
|
///
|
||||||
|
/// #other-elem(elem[This element will be matched and removed])
|
||||||
|
/// #elem[This element stays, as it is not inside `other-elem`]
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// - ancestor-filter (filter): a filter to match potential ancestors.
|
||||||
|
/// - depth (int): only match at this exact KNOWN depth.
|
||||||
|
/// - max-depth (int): only match up to this exact KNOWN depth.
|
||||||
|
/// -> filter
|
||||||
|
#let within-filter(ancestor-filter, depth: none, max-depth: none) = {
|
||||||
|
if type(ancestor-filter) == function {
|
||||||
|
ancestor-filter = ancestor-filter(__elembic_data: special-data-values.get-where)
|
||||||
|
}
|
||||||
|
assert(type(ancestor-filter) == dictionary and filter-key in ancestor-filter, message: "elembic: filters.within: invalid filter, please use 'custom-element.with(...)' to generate a filter.")
|
||||||
|
assert("elements" in ancestor-filter, message: "elembic: filters.within: the ancestor filter is missing the 'elements' field; this indicates it comes from an element generated with an outdated elembic version. Please use an element made with an up-to-date elembic version.")
|
||||||
|
assert(ancestor-filter.elements != (:), message: "elembic: filters.within: the ancestor filter appears to not be restricted to any elements and is thus impossible to match. It must apply to at least one element (potential ancestor). Consider using a different filter.")
|
||||||
|
assert(ancestor-filter.elements != none, message: "elembic: filters.within: the ancestor filter appears to apply to any element. It must apply to exactly one element (the one receiving the set rule). Consider using an 'and' filter, e.g. 'e.filters.within(e.filters.and(wibble, e.not(wibble.with(a: 10))))' instead of just 'e.filters.within(e.not(wibble.with(a: 10)))', to restrict it.")
|
||||||
|
assert(depth == none or max-depth == none, message: "elembic: filters.within: cannot specify both depth and max-depth (please pick one).")
|
||||||
|
assert(depth == none or type(depth) == int and depth > 0, message: "elembic: filters.within: 'depth' parameter must be a positive integer or 'none'.")
|
||||||
|
assert(max-depth == none or type(max-depth) == int and max-depth > 0, message: "elembic: filters.within: 'max-depth' parameter must be a positive integer or 'none'.")
|
||||||
|
|
||||||
|
(
|
||||||
|
(filter-key): true,
|
||||||
|
element-version: element-version,
|
||||||
|
kind: "within",
|
||||||
|
ancestor-filter: ancestor-filter,
|
||||||
|
depth: depth,
|
||||||
|
max-depth: max-depth,
|
||||||
|
elements: none,
|
||||||
|
ancestry-elements: (:) + ancestor-filter.elements + ancestor-filter.at("ancestry-elements", default: (:)),
|
||||||
|
may-need-ancestry: true,
|
||||||
|
)
|
||||||
|
}
|
||||||
12
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/lib.typ
vendored
Normal file
12
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/lib.typ
vendored
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
#import "element.typ": set_, data, apply, revoke, reset, named, filtered, cond-set, show_, style-modes, prepare-get as get, settings, elem-selector as selector, select, elem-query as query, ref_ as ref, prepare
|
||||||
|
#import "fields.typ": field
|
||||||
|
#import "filters.typ": within-filter as within
|
||||||
|
#import "pub/data.typ": *
|
||||||
|
#import "pub/element.typ"
|
||||||
|
#import "pub/filters.typ"
|
||||||
|
#import "pub/parsing.typ"
|
||||||
|
#import "pub/types.typ"
|
||||||
|
#import "pub/result.typ"
|
||||||
|
#import "pub/leaky.typ"
|
||||||
|
#import "pub/stateful.typ"
|
||||||
|
#import "pub/internal.typ"
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/data.typ
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/data.typ
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "../data.typ": fields, counter_ as counter, ctx, scope, func, func-name, eid, tid, repr_ as repr, eq
|
||||||
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/element.typ
vendored
Normal file
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/element.typ
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Public re-exports for element-related functions.
|
||||||
|
#import "../element.typ": declare
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/filters.typ
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/filters.typ
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "../filters.typ": or-filter as or_, and-filter as and_, not-filter as not_, xor-filter as xor, custom-filter as custom
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/internal-constants.typ
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/internal-constants.typ
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "../data.typ": lbl-show-head, lbl-meta-head, lbl-outer-head, lbl-counter-head, lbl-elem-prepare-check-head, lbl-empty-prepare-check, labelable-elem-figure-kind, lbl-ref-figure-kind-head, lbl-ref-figure-label-head, lbl-ref-figure, lbl-get, lbl-tag, lbl-rule-tag, lbl-old-rule-tag, lbl-special-rule-tag, lbl-data-metadata, lbl-stateful-mode, lbl-leaky-mode, lbl-normal-mode, lbl-auto-mode, lbl-global-select-head, prepared-rule-key, stored-data-key, element-key, element-data-key, element-meta-key, global-data-key, filter-key, special-rule-key, special-data-values, custom-type-key, custom-type-data-key, type-key, element-version, type-version, custom-type-version, current-field-version, style-modes
|
||||||
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/internal.typ
vendored
Normal file
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/internal.typ
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
#import "../element.typ": prepare-debug as debug-get, stateful-debug-get
|
||||||
|
#import "internal-constants.typ" as constants
|
||||||
8
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/leaky.typ
vendored
Normal file
8
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/leaky.typ
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Exports rules defaulting to leaky mode.
|
||||||
|
#import "../element.typ": leaky-set as set_, leaky-apply as apply, leaky-show as show_, leaky-revoke as revoke, leaky-reset as reset, leaky-cond-set as cond-set, leaky-settings as settings, leaky-toggle as toggle
|
||||||
|
|
||||||
|
/// Enable leaky mode by default.
|
||||||
|
#let enable = toggle.with(true)
|
||||||
|
|
||||||
|
/// Disable leaky mode by default.
|
||||||
|
#let disable = toggle.with(false)
|
||||||
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/native.typ
vendored
Normal file
2
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/native.typ
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
// Public re-exports for native type-related functions and constants.
|
||||||
|
#import "../types/native.typ": content_, auto_, none_, float_, function_, int_, array_, dict_, datetime_, duration_, color_, gradient_, str_, type_, bool_, relative_, ratio_, typeinfo, angle_, arguments_, bytes_, tiling, tiling_, version_, fraction_, length_, stroke_
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/parsing.typ
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/parsing.typ
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "../fields.typ": parse-fields, generate-arg-parser
|
||||||
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/result.typ
vendored
Normal file
1
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/result.typ
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
#import "../types/base.typ": ok, err, is-ok
|
||||||
8
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/stateful.typ
vendored
Normal file
8
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/stateful.typ
vendored
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Exports rules defaulting to stateful mode.
|
||||||
|
#import "../element.typ": toggle-stateful-mode as toggle, stateful-set as set_, stateful-apply as apply, stateful-show as show_, stateful-revoke as revoke, stateful-reset as reset, stateful-cond-set as cond-set, stateful-get as get, stateful-settings as settings
|
||||||
|
|
||||||
|
// Enable stateful mode.
|
||||||
|
#let enable = toggle.with(true)
|
||||||
|
|
||||||
|
// Disable stateful mode.
|
||||||
|
#let disable = toggle.with(false)
|
||||||
5
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/types.typ
vendored
Normal file
5
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/pub/types.typ
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
// Public re-exports for type-related functions and constants.
|
||||||
|
#import "../types/base.typ": any, never, custom-type, typeid, typename, native-elem
|
||||||
|
#import "../types/types.typ": option, smart, union, paint, literal, exact, wrap, array_ as array, dict_ as dict, default, validate as typeinfo, typeof, cast, generate-cast-error
|
||||||
|
#import "../types/custom.typ": declare
|
||||||
|
#import "native.typ"
|
||||||
694
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/base.typ
vendored
Normal file
694
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/base.typ
vendored
Normal file
@@ -0,0 +1,694 @@
|
|||||||
|
// The shared fundamentals of the type system.
|
||||||
|
#import "../data.typ": data, type-key, custom-type-key, custom-type-data-key, repr_, func-name, type-version, eq, elem-funcs
|
||||||
|
|
||||||
|
// Typeinfo structure:
|
||||||
|
// - type-key: kind of type
|
||||||
|
// - version: 1
|
||||||
|
// - name: type name
|
||||||
|
// - input: list of native types / custom types of input
|
||||||
|
// - output: list of native types / custom types of output
|
||||||
|
// - data: data specific for this type key
|
||||||
|
// - check: none (only check inputs) or function x => bool
|
||||||
|
// - cast: none (input is unchanged) or function to convert input to output
|
||||||
|
// - error: none or function x => string to customize check failure message
|
||||||
|
// - default: empty array (no default) or singleton array => default value for this type
|
||||||
|
// - fold: none, auto (equivalent to (a, b) => a + b but more efficient) or function (prev, next) => folded value:
|
||||||
|
// determines how to combine two consecutive values of this type in the stylechain
|
||||||
|
#let base-typeinfo = (
|
||||||
|
(type-key): true,
|
||||||
|
type-kind: "base",
|
||||||
|
version: type-version,
|
||||||
|
name: "unknown",
|
||||||
|
input: (),
|
||||||
|
output: (),
|
||||||
|
data: none,
|
||||||
|
check: none,
|
||||||
|
cast: none,
|
||||||
|
error: none,
|
||||||
|
default: (),
|
||||||
|
fold: none,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Top type
|
||||||
|
// input and output have "any".
|
||||||
|
#let any = (
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "any",
|
||||||
|
name: "any",
|
||||||
|
input: ("any",),
|
||||||
|
output: ("any",),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Bottom type
|
||||||
|
// input and output are empty.
|
||||||
|
#let never = (
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "never",
|
||||||
|
name: "never",
|
||||||
|
input: (),
|
||||||
|
output: (),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Any custom type
|
||||||
|
#let custom-type = (
|
||||||
|
..base-typeinfo,
|
||||||
|
(type-key): "custom type",
|
||||||
|
name: "custom type",
|
||||||
|
input: ("custom type",),
|
||||||
|
output: ("custom type",),
|
||||||
|
)
|
||||||
|
|
||||||
|
#let element(name, eid) = (
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "element",
|
||||||
|
name: "element '" + name + "'",
|
||||||
|
input: (content,),
|
||||||
|
output: (content,),
|
||||||
|
check: c => c.func() in elem-funcs and data(c).eid == eid,
|
||||||
|
data: (name: name, eid: eid),
|
||||||
|
error: c => "expected element " + name + ", found " + func-name(c),
|
||||||
|
)
|
||||||
|
|
||||||
|
#let native-elem(func) = {
|
||||||
|
assert(type(func) == function, message: "elembic: types.native-elem: expected native element constructor, got " + str(type(func)))
|
||||||
|
|
||||||
|
(
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "native-element",
|
||||||
|
name: "native element '" + repr(func) + "'",
|
||||||
|
input: (content,),
|
||||||
|
output: (content,),
|
||||||
|
check: if func in elem-funcs { c => c.func() == func and data(c).eid == none } else { c => c.func() == func },
|
||||||
|
data: (func: func),
|
||||||
|
error: c => "expected native element " + repr(func) + ", found " + func-name(c),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the type ID of a value.
|
||||||
|
// This is usually 'type(value)', unless value has a custom type.
|
||||||
|
// In that case, it has the format '(tid: ..., name: ...)'.
|
||||||
|
// This is the format expected by 'input' and 'output' arrays.
|
||||||
|
#let typeid(value) = {
|
||||||
|
let value-type = type(value)
|
||||||
|
if value-type == dictionary and custom-type-key in value {
|
||||||
|
value-type = value.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
value-type
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the name of the value's type as a string.
|
||||||
|
#let typename(value) = {
|
||||||
|
let value-type = type(value)
|
||||||
|
if value-type == dictionary and custom-type-key in value {
|
||||||
|
let id = value.at(custom-type-key).id
|
||||||
|
if "name" in id {
|
||||||
|
id.name
|
||||||
|
} else {
|
||||||
|
str(id)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
str(value-type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a unique element or type ID based on prefix and name.
|
||||||
|
//
|
||||||
|
// Uses a separator and a "bit stuffing" technique to ensure
|
||||||
|
// the separator sequence doesn't appear in either of the
|
||||||
|
// prefix or the name in the final ID.
|
||||||
|
#let id-separator = "_---_"
|
||||||
|
#let trimmed-separator = id-separator.trim("_", at: end)
|
||||||
|
#let unique-id(kind, prefix, name) = {
|
||||||
|
(
|
||||||
|
kind + "_"
|
||||||
|
) + prefix.replace(
|
||||||
|
trimmed-separator, trimmed-separator + "-"
|
||||||
|
) + id-separator + name.replace(
|
||||||
|
trimmed-separator, trimmed-separator + "-"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal type
|
||||||
|
// Only accepted if value is equal to the literal.
|
||||||
|
// Input and output are equal to the value.
|
||||||
|
//
|
||||||
|
// Uses base typeinfo information for information such as casts and whatnot.
|
||||||
|
#let literal(value, typeinfo) = {
|
||||||
|
let represented = "'" + if type(value) == str { value } else { repr_(value) } + "'"
|
||||||
|
let value-type = typeid(value)
|
||||||
|
|
||||||
|
let check = if typeinfo.check == none { x => eq(x, value) } else { x => eq(x, value) and (typeinfo.check)(x) }
|
||||||
|
|
||||||
|
(
|
||||||
|
..typeinfo,
|
||||||
|
type-kind: "literal",
|
||||||
|
name: "literal " + represented,
|
||||||
|
data: (value: value, typeinfo: typeinfo, represented: represented),
|
||||||
|
check: check,
|
||||||
|
error: _ => "given value wasn't equal to literal " + represented,
|
||||||
|
default: (value,),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Union type (one of many)
|
||||||
|
// Data is the list of typeinfos.
|
||||||
|
// Accepted if the value corresponds to one of the given types.
|
||||||
|
// Does not check the validity of typeinfos.
|
||||||
|
#let union(typeinfos) = {
|
||||||
|
// Flatten nested unions
|
||||||
|
let typeinfos = typeinfos.map(t => if t.type-kind == "union" { t.data } else { (t,) }).sum(default: ()).dedup()
|
||||||
|
if typeinfos == () {
|
||||||
|
// No inputs accepted...
|
||||||
|
return never
|
||||||
|
}
|
||||||
|
if typeinfos.len() == 1 {
|
||||||
|
// Simplify union if there's nothing else
|
||||||
|
return typeinfos.first()
|
||||||
|
}
|
||||||
|
if typeinfos.any(x => x.type-kind == "any") {
|
||||||
|
// Union with 'any' is just any
|
||||||
|
return any
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = typeinfos.map(t => t.name).join(", ", last: " or ")
|
||||||
|
let input = typeinfos.map(t => t.input).sum(default: ()).dedup()
|
||||||
|
let output = typeinfos.map(t => t.output).sum(default: ()).dedup()
|
||||||
|
|
||||||
|
let has-any-input = "any" in input
|
||||||
|
let has-any-output = "any" in output
|
||||||
|
|
||||||
|
if has-any-input {
|
||||||
|
input = ("any",)
|
||||||
|
}
|
||||||
|
|
||||||
|
if has-any-output {
|
||||||
|
output = ("any",)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to optimize checks as much as possible
|
||||||
|
let check = if typeinfos.all(t => t.check == none) {
|
||||||
|
// If there are no checks, just checking inputs is enough
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
let checked-types = typeinfos.filter(t => t.check != none)
|
||||||
|
let unchecked-inputs = typeinfos.filter(t => t.check == none).map(t => t.input).sum(default: ()).dedup()
|
||||||
|
if input.all(t => t in unchecked-inputs) {
|
||||||
|
// Unchecked types include all possible input types, so some check will always succeed
|
||||||
|
// Note that this check also works for input reduced to just "any". If "any" is an
|
||||||
|
// unchecked input, then checks will never fail.
|
||||||
|
none
|
||||||
|
} else if checked-types.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
// From here onwards, we can assume unchecked-inputs doesn't contain "any",
|
||||||
|
// since it is a subset of input, therefore input would be just ("any",) and
|
||||||
|
// the check above would have had to pass in that case.
|
||||||
|
let all-funcs = checked-types.map(t => t.data.func)
|
||||||
|
let non-elem-funcs = all-funcs.filter(f => f not in elem-funcs)
|
||||||
|
let has-elem-func = elem-funcs.any(f => f in all-funcs)
|
||||||
|
|
||||||
|
// Check sequence separately, as a sequence can also be a custom element,
|
||||||
|
// so we must tell them apart.
|
||||||
|
if has-elem-func {
|
||||||
|
if non-elem-funcs == () {
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
typ in unchecked-inputs or typ == content and x.func() in elem-funcs and data(x).eid == none
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
typ in unchecked-inputs or typ == content and (x.func() in non-elem-funcs or x.func() in elem-funcs and data(x).eid == none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
typ in unchecked-inputs or typ == content and x.func() in non-elem-funcs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if checked-types.all(t => t.type-kind == "element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
let all-eids = checked-types.map(t => t.data.eid)
|
||||||
|
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
typ in unchecked-inputs or typ == content and x.func() in elem-funcs and data(x).eid in all-eids
|
||||||
|
}
|
||||||
|
} else if checked-types.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
let values-inputs-and-checks = checked-types.map(t => (t.data.value, t.input, t.data.typeinfo.check))
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
typ in unchecked-inputs or values-inputs-and-checks.any(((v, i, check)) => eq(x, v) and (typ in i or "any" in i) and (check == none or check(x)))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If any check succeeds and the value has the correct input type, OK
|
||||||
|
let checks-and-inputs = checked-types.map(t => (t.input, t.check))
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
// If one of the types without checks accepts this type as an input then we don't need
|
||||||
|
// to run any checks!
|
||||||
|
typ in unchecked-inputs or checks-and-inputs.any(((inp, check)) => (typ in inp or "any" in inp) and check(x))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to optimize casts
|
||||||
|
let cast = if typeinfos.all(t => t.cast == none) {
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
let casting-types = typeinfos.filter(t => t.cast != none)
|
||||||
|
let first-casting-type = casting-types.first()
|
||||||
|
if (
|
||||||
|
// If the casting types are all native, and none of the types before them
|
||||||
|
// accept their "cast-from" types, then we can fast track to a simple check:
|
||||||
|
// if within the 'cast-from' types, then cast, otherwise don't.
|
||||||
|
casting-types != ()
|
||||||
|
and casting-types.all(t => t.type-kind == "native" and t.data in (float, content) and ("__future_cast" not in t or t.__future_cast.max-version < type-version))
|
||||||
|
and typeinfos.find(t => t.input.any(i => i == "any" or i in first-casting-type.input)) == first-casting-type
|
||||||
|
and (casting-types.len() == 1 or typeinfos.find(t => t.input.any(i => i == "any" or i in casting-types.at(1).input)) == casting-types.at(1))
|
||||||
|
) {
|
||||||
|
if casting-types.len() >= 2 { // just float and content
|
||||||
|
x => if type(x) == int { float(x) } else if x == none or type(x) in (str, symbol) [#x] else { x }
|
||||||
|
} else if first-casting-type.data == float { // just float
|
||||||
|
x => if type(x) == int { float(x) } else { x }
|
||||||
|
} else { // just content
|
||||||
|
x => if x == none or type(x) in (str, symbol) { [#x] } else { x }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Generic case
|
||||||
|
x => {
|
||||||
|
let typ = type(x)
|
||||||
|
if typ == dictionary and custom-type-key in x {
|
||||||
|
// Custom type must be checked differently in inputs
|
||||||
|
typ = x.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
let typeinfo = typeinfos.find(t => (typ in t.input or "any" in t.input) and (t.check == none or (t.check)(x)))
|
||||||
|
if typeinfo.cast == none {
|
||||||
|
x
|
||||||
|
} else {
|
||||||
|
(typeinfo.cast)(x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let error = if typeinfos.all(t => t.error == none) {
|
||||||
|
none
|
||||||
|
} else if typeinfos.all(t => t.type-kind == "literal" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
let literals = typeinfos.map(t => str(t.data.represented)).join(", ", last: " or ")
|
||||||
|
let message = "given value wasn't equal to literals " + literals
|
||||||
|
x => message
|
||||||
|
} else if typeinfos.all(t => t.type-kind == "native-element" and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
let funcs = typeinfos.map(t => repr(t.data.func)).join(", ", last: " or ")
|
||||||
|
let head = "expected native elements " + funcs + ", found "
|
||||||
|
x => head + {
|
||||||
|
if type(x) == content { func-name(x) } else { "a(n) " + typename(x) }
|
||||||
|
}
|
||||||
|
} else if typeinfos.all(t => (t.type-kind == "element" or t.type-kind == "native-element") and ("__future_cast" not in t or t.__future_cast.max-version < type-version)) {
|
||||||
|
let funcs = typeinfos.map(t => if t.type-kind == "element" { t.data.name } else { repr(t.data.func) + " (native)" }).join(", ", last: " or ")
|
||||||
|
let head = "expected elements " + funcs + ", found "
|
||||||
|
x => head + {
|
||||||
|
if type(x) == content { func-name(x) } else { "a(n) " + typename(x) }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let error-types = typeinfos.filter(t => t.error != none)
|
||||||
|
x => {
|
||||||
|
"all typechecks for union failed" + error-types.filter(t => typeid(x) in t.input).map(t => "\n hint (" + t.name + "): " + (t.error)(x)).sum(default: "")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let is-option = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(none)
|
||||||
|
let is-smart = typeinfos.first().type-kind == "native" and typeinfos.first().data == type(auto)
|
||||||
|
|
||||||
|
if is-smart != is-option and typeinfos.len() == 3 and typeinfos.at(1).type-kind == "native" and typeinfos.at(1).data in (type(none), type(auto)) {
|
||||||
|
// Both first types are (none, auto)
|
||||||
|
is-smart = true;
|
||||||
|
is-option = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default = if is-option or is-smart {
|
||||||
|
// Default of 'none' for option(...)
|
||||||
|
// Default of 'auto' for smart(...)
|
||||||
|
typeinfos.first().default
|
||||||
|
} else {
|
||||||
|
()
|
||||||
|
}
|
||||||
|
|
||||||
|
let fold = if typeinfos.all(t => t.fold == none) or typeinfos.any(t => "any" in t.output) {
|
||||||
|
// We'd like to handle folding with "any" output in the future, but for now, let's not
|
||||||
|
none
|
||||||
|
} else if (is-option or is-smart) and typeinfos.len() == 2 and typeinfos.last().fold != none {
|
||||||
|
// Match built-in behavior by only folding option(T) or smart(T) if T can fold and the inner isn't explicitly none/auto
|
||||||
|
let other-typeinfo = typeinfos.at(1)
|
||||||
|
let other-fold = other-typeinfo.fold
|
||||||
|
if is-option {
|
||||||
|
if other-fold == auto {
|
||||||
|
(outer, inner) => if inner != none and outer != none { outer + inner } else { inner }
|
||||||
|
} else {
|
||||||
|
(outer, inner) => if inner != none and outer != none { other-fold(outer, inner) } else { inner }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if other-fold == auto {
|
||||||
|
(outer, inner) => if inner != auto and outer != auto { outer + inner } else { inner }
|
||||||
|
} else {
|
||||||
|
(outer, inner) => if inner != auto and outer != auto { other-fold(outer, inner) } else { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if is-option and is-smart and typeinfos.len() == 3 and typeinfos.last().fold != none {
|
||||||
|
// smart(option(T))
|
||||||
|
// and option(smart(T))
|
||||||
|
let other-typeinfo = typeinfos.last()
|
||||||
|
let other-fold = other-typeinfo.fold
|
||||||
|
if other-fold == auto {
|
||||||
|
(outer, inner) => if inner != none and inner != auto and outer != none and outer != auto { outer + inner } else { inner }
|
||||||
|
} else {
|
||||||
|
(outer, inner) => if inner != none and inner != auto and outer != none and outer != auto { other-fold(outer, inner) } else { inner }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Arbitrary union folding is allowed if the different casted values would
|
||||||
|
// belong to the same union type.
|
||||||
|
// Otherwise, can't do much if e.g. an int could be typeinfo A (say, positive integer)
|
||||||
|
// or typeinfo B (say, negative integer) because checks apply to inputs and not outputs
|
||||||
|
// (unless, of course, there is no casting).
|
||||||
|
// However, if there is only one typeinfo with a given output type, it is
|
||||||
|
// not ambiguous and may fold.
|
||||||
|
let unsure-outputs = () // array of (output type)
|
||||||
|
let unsure-output-data = () // array of ((i, fold, output))
|
||||||
|
let ambiguous-outputs = () // array of (output type)
|
||||||
|
|
||||||
|
for (i, typeinfo) in typeinfos.enumerate() {
|
||||||
|
for output in typeinfo.output.dedup() {
|
||||||
|
// NOTE: we assume 'output != "any"' as we filter that out in a
|
||||||
|
// previous conditional.
|
||||||
|
if output in unsure-outputs {
|
||||||
|
ambiguous-outputs.push(output)
|
||||||
|
|
||||||
|
// Invariant: there are no duplicate output types in 'unsure-outputs'.
|
||||||
|
// The only time we push to unsure-outputs is after this check fails,
|
||||||
|
// so that is always true.
|
||||||
|
let output-index = unsure-outputs.position(t => t == output)
|
||||||
|
_ = unsure-outputs.remove(output-index)
|
||||||
|
_ = unsure-output-data.remove(output-index)
|
||||||
|
} else if output != "never" and output not in ambiguous-outputs {
|
||||||
|
unsure-outputs.push(output)
|
||||||
|
unsure-output-data.push((i, typeinfo.fold, output))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// No conflicts found for those types
|
||||||
|
let unambiguous-outputs = unsure-output-data
|
||||||
|
|
||||||
|
let func(outer, inner) = {
|
||||||
|
let outer-id = typeid(outer)
|
||||||
|
let inner-id = typeid(inner)
|
||||||
|
let outer-output = unambiguous-outputs.find(((_, _, output)) => outer-id == output)
|
||||||
|
let inner-output = unambiguous-outputs.find(((_, _, output)) => inner-id == output)
|
||||||
|
let folder = if inner-output == none or outer-output == none or outer-output.first() != inner-output.first() {
|
||||||
|
// They belong to different types in the union, so no folding can be
|
||||||
|
// performed, even if they have the same fold function, since it still
|
||||||
|
// expects them to have the appropriate type.
|
||||||
|
//
|
||||||
|
// This branch is also reached when neither type is found. This
|
||||||
|
// suggests both belong to ambiguous output types and must not
|
||||||
|
// be folded.
|
||||||
|
return inner
|
||||||
|
} else {
|
||||||
|
// They belong to the same type with the same fold
|
||||||
|
outer-output.at(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if folder == none {
|
||||||
|
inner
|
||||||
|
} else if folder == auto {
|
||||||
|
// Caution: addition order matters!
|
||||||
|
// Could be arrays, for example.
|
||||||
|
outer + inner
|
||||||
|
} else {
|
||||||
|
folder(outer, inner)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if unambiguous-outputs == () {
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
func
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "union",
|
||||||
|
name: name,
|
||||||
|
data: typeinfos,
|
||||||
|
input: input,
|
||||||
|
output: output,
|
||||||
|
check: check,
|
||||||
|
cast: cast,
|
||||||
|
error: error,
|
||||||
|
default: default,
|
||||||
|
fold: fold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result to indicate success and return a value.
|
||||||
|
#let ok(value) = {
|
||||||
|
(true, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A result to indicate failure, with an error value indicating what happened.
|
||||||
|
#let err(error) = {
|
||||||
|
(false, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Whether this result was successful.
|
||||||
|
#let is-ok(result) = {
|
||||||
|
type(result) == array and result.len() == 2 and result.first() == true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wrap a typeinfo with some other data.
|
||||||
|
// Mostly unchecked variant of 'types.wrap'.
|
||||||
|
#let wrap(typeinfo, overrides) = {
|
||||||
|
(
|
||||||
|
(..typeinfo, type-kind: "wrapped", data: (base: typeinfo, extra: none))
|
||||||
|
+ for (key, default) in base-typeinfo {
|
||||||
|
if key == type-key or key == "type-kind" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if key in overrides {
|
||||||
|
let override = overrides.at(key)
|
||||||
|
|
||||||
|
if key == "data" {
|
||||||
|
(data: (base: typeinfo, extra: override))
|
||||||
|
} else {
|
||||||
|
((key): override)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// A particular collection of types.
|
||||||
|
#let collection(name, base, parameters, check: none, cast: none, error: none, ..args) = {
|
||||||
|
if check == none {
|
||||||
|
check = base.check
|
||||||
|
}
|
||||||
|
|
||||||
|
if cast == none {
|
||||||
|
cast = base.cast
|
||||||
|
}
|
||||||
|
|
||||||
|
if check == none and error == none {
|
||||||
|
error = base.error
|
||||||
|
}
|
||||||
|
|
||||||
|
let other-args = args.named()
|
||||||
|
let default = if "default" in other-args {
|
||||||
|
other-args.default
|
||||||
|
} else {
|
||||||
|
base.default
|
||||||
|
}
|
||||||
|
|
||||||
|
let fold = if "fold" in other-args {
|
||||||
|
other-args.fold
|
||||||
|
} else {
|
||||||
|
base.fold
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
..base,
|
||||||
|
type-kind: "collection",
|
||||||
|
name: name + if parameters != () { " of " + parameters.map(t => t.name).join(", ", last: " and ") },
|
||||||
|
data: (base: base, parameters: parameters),
|
||||||
|
check: check,
|
||||||
|
cast: cast,
|
||||||
|
error: error,
|
||||||
|
default: default,
|
||||||
|
fold: fold,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create an array collection with a uniform parameter typeinfo for its elements.
|
||||||
|
#let array_(base-type, param, error: none) = {
|
||||||
|
assert(array in base-type.input or "any" in base-type.input)
|
||||||
|
let kind = param.type-kind
|
||||||
|
|
||||||
|
collection(
|
||||||
|
"array",
|
||||||
|
base-type,
|
||||||
|
(param,),
|
||||||
|
check: if param.check == none and "any" in param.input {
|
||||||
|
none
|
||||||
|
} else if param.input == () {
|
||||||
|
// Propagate 'never'
|
||||||
|
_ => false
|
||||||
|
} else if "any" in param.input {
|
||||||
|
// Only need to run checks
|
||||||
|
a => a.all(param.check)
|
||||||
|
} else {
|
||||||
|
// Some optimizations ahead
|
||||||
|
// The proper code is at the bottom
|
||||||
|
let input = param.input
|
||||||
|
let check = param.check
|
||||||
|
if kind == "native" and param.data == dictionary and ("__future_cast" not in param or param.__future_cast.max-version < type-version) {
|
||||||
|
a => a.all(x => type(x) == dictionary and custom-type-key not in x)
|
||||||
|
} else if param.input.all(i => type(i) == type) and dictionary not in param.input {
|
||||||
|
// No custom types accepted (the check above excludes '(tid: ..., name: ...)' as well as "any")
|
||||||
|
// If this is a custom type, it will return type(x) = dictionary, so it will fail
|
||||||
|
// (Also excludes "custom type": the type of custom types)
|
||||||
|
// So that suffices
|
||||||
|
if input.len() == 1 {
|
||||||
|
let input = input.first()
|
||||||
|
if check == none {
|
||||||
|
a => a.all(x => type(x) == input)
|
||||||
|
} else {
|
||||||
|
a => a.all(x => type(x) == input and check(x))
|
||||||
|
}
|
||||||
|
} else if input.len() == 2 {
|
||||||
|
let first = input.first()
|
||||||
|
let second = input.at(1)
|
||||||
|
if check == none {
|
||||||
|
a => a.all(x => type(x) == first or type(x) == second)
|
||||||
|
} else {
|
||||||
|
a => a.all(x => (type(x) == first or type(x) == second) and check(x))
|
||||||
|
}
|
||||||
|
} else if check == none {
|
||||||
|
a => a.all(x => type(x) in input)
|
||||||
|
} else {
|
||||||
|
a => a.all(x => type(x) in input and check(x))
|
||||||
|
}
|
||||||
|
} else if param.check == none {
|
||||||
|
a => a.all(x => typeid(x) in param.input)
|
||||||
|
} else {
|
||||||
|
a => a.all(x => typeid(x) in param.input and check(x))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cast: if param.cast == none {
|
||||||
|
none
|
||||||
|
} else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) {
|
||||||
|
a => a.map(x => [#x])
|
||||||
|
} else {
|
||||||
|
a => a.map(param.cast)
|
||||||
|
},
|
||||||
|
|
||||||
|
error: error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dict with a uniform parameter typeinfo for its values.
|
||||||
|
// (Keys are always strings.)
|
||||||
|
#let dict_(base-type, param, error: none) = {
|
||||||
|
assert(dictionary in base-type.input or "any" in base-type.input)
|
||||||
|
let kind = param.type-kind
|
||||||
|
|
||||||
|
collection(
|
||||||
|
"dict",
|
||||||
|
base-type,
|
||||||
|
(param,),
|
||||||
|
check: {
|
||||||
|
// Simply check the array of values
|
||||||
|
// (We can pass 'any' as the base type since that doesn't affect the 'check')
|
||||||
|
let array-check = array_(any, param).check
|
||||||
|
if array-check == none {
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
d => array-check(d.values())
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
cast: if param.cast == none {
|
||||||
|
none
|
||||||
|
} else if kind == "native" and param.data == content and ("__future_cast" not in param or param.__future_cast.max-version < type-version) {
|
||||||
|
d => {
|
||||||
|
for (k, v) in d {
|
||||||
|
d.at(k) = [#v]
|
||||||
|
}
|
||||||
|
d
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let cast = param.cast
|
||||||
|
d => {
|
||||||
|
for (k, v) in d {
|
||||||
|
d.at(k) = cast(v)
|
||||||
|
}
|
||||||
|
d
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
error: error,
|
||||||
|
|
||||||
|
fold: if param.fold == none {
|
||||||
|
base-type.fold
|
||||||
|
} else if param.fold == auto {
|
||||||
|
(outer, inner) => {
|
||||||
|
let combined = outer + inner
|
||||||
|
|
||||||
|
for (k, v) in outer {
|
||||||
|
if k in inner {
|
||||||
|
combined.at(k) = v + inner.at(k)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
} else if type(param.fold) == function {
|
||||||
|
(outer, inner) => {
|
||||||
|
let combined = outer + inner
|
||||||
|
|
||||||
|
for (k, v) in outer {
|
||||||
|
if k in inner {
|
||||||
|
combined.at(k) = (param.fold)(v, inner.at(k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.dict: parameter didn't have a valid fold function")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
428
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/custom.typ
vendored
Normal file
428
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/custom.typ
vendored
Normal file
@@ -0,0 +1,428 @@
|
|||||||
|
// Custom types!
|
||||||
|
#import "../data.typ": special-data-values, custom-type-key, custom-type-data-key, type-key, custom-type-version
|
||||||
|
#import "base.typ"
|
||||||
|
#import "types.typ"
|
||||||
|
#import "../fields.typ" as field-internals
|
||||||
|
|
||||||
|
// Default folding procedure for custom types.
|
||||||
|
// Combines each inner type individually.
|
||||||
|
#let auto-fold(foldable-fields) = if foldable-fields == (:) {
|
||||||
|
// No fields to fold, so 'inner' always fully overwrites 'outer'.
|
||||||
|
// In that case, we can just sum inner with outer, adding its fields
|
||||||
|
// on top.
|
||||||
|
auto
|
||||||
|
} else {
|
||||||
|
(outer, inner) => {
|
||||||
|
let combined = outer + inner
|
||||||
|
|
||||||
|
for (field-name, fold-data) in foldable-fields {
|
||||||
|
if field-name in inner {
|
||||||
|
let outer = outer.at(field-name, default: fold-data.default)
|
||||||
|
if fold-data.folder == auto {
|
||||||
|
combined.at(field-name) = outer + inner.at(field-name)
|
||||||
|
} else {
|
||||||
|
combined.at(field-name) = (fold-data.folder)(outer, inner.at(field-name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
combined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let auto-cast(from, fields: (:), constructor: none) = {
|
||||||
|
if from == dictionary {
|
||||||
|
value => constructor(..value)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let auto-cast-check(from, fields: (:), parse-args: none) = {
|
||||||
|
if from == dictionary {
|
||||||
|
value => parse-args(arguments(..value)).first()
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let auto-cast-error(from, fields: (:), parse-args: none) = {
|
||||||
|
if from == dictionary {
|
||||||
|
value => parse-args(arguments(..value)).at(1)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.auto-cast: invalid auto cast type: 'from' must be dictionary.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let declare(
|
||||||
|
name,
|
||||||
|
fields: none,
|
||||||
|
prefix: none,
|
||||||
|
doc: none,
|
||||||
|
default: none,
|
||||||
|
parse-args: auto,
|
||||||
|
typecheck: true,
|
||||||
|
allow-unknown-fields: false,
|
||||||
|
construct: none,
|
||||||
|
scope: none,
|
||||||
|
casts: none,
|
||||||
|
fold: auto,
|
||||||
|
) = {
|
||||||
|
|
||||||
|
let fields-hint = if type(fields) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single field: write 'fields: (field,)', not 'fields: (field)'" } else { "" }
|
||||||
|
let casts-hint = if type(casts) == dictionary { "\n hint: check if you didn't forget to add a trailing comma for a single cast: write 'casts: ((from: ..., with: ...),)', not 'casts: ((from: ..., with: ...))'" } else { "" }
|
||||||
|
assert(type(fields) == array, message: "elembic: types.declare: please specify an array of fields, creating each field with the 'field' function." + fields-hint)
|
||||||
|
assert(prefix != none, message: "elembic: types.declare: please specify a 'prefix: ...' for your type, to distinguish it from types with the same name. If you are writing a package or template to be used by others, please do not use an empty prefix.")
|
||||||
|
assert(type(prefix) == str, message: "elembic: types.declare: the prefix must be a string, not '" + str(type(prefix)) + "'")
|
||||||
|
assert(doc == none or type(doc) == str, message: "elembic: types.declare: 'doc' must be none or a string (add documentation)")
|
||||||
|
assert(parse-args == auto or type(parse-args) == function, message: "elembic: types.declare: 'parse-args' must be either 'auto' (use built-in parser) or a function (default arg parser, fields: dictionary, typecheck: bool) => (user arguments, include-required: true) => (bool (true on success, false on error), dictionary with parsed fields (or error message string if the bool is false)).")
|
||||||
|
assert(type(typecheck) == bool, message: "elembic: types.declare: the 'typecheck' argument must be a boolean (true to enable typechecking in the constructor, false to disable).")
|
||||||
|
assert(type(allow-unknown-fields) == bool, message: "elembic: types.declare: the 'allow-unknown-fields' argument must be a boolean.")
|
||||||
|
assert(construct == none or type(construct) == function, message: "elembic: types.declare: 'construct' must be 'none' (use default constructor) or a function receiving the original constructor and returning the new constructor.")
|
||||||
|
assert(default == none or type(default) == function, message: "elembic: types.declare: 'default' must be none or a function receiving the constructor and returning the default.")
|
||||||
|
assert(scope == none or type(scope) in (dictionary, module), message: "elembic: types.declare: 'scope' must be either 'none', a dictionary or a module")
|
||||||
|
assert(
|
||||||
|
casts == none
|
||||||
|
or type(casts) == array and casts.all(
|
||||||
|
d => (
|
||||||
|
type(d) == dictionary
|
||||||
|
and "from" in d
|
||||||
|
and d.keys().all(k => k in ("from", "with", "check"))
|
||||||
|
and ("with" not in d or type(d.with) == function)
|
||||||
|
and ("check" not in d or d.check == none or type(d.check) == function)
|
||||||
|
)
|
||||||
|
),
|
||||||
|
message: "elembic: types.declare: 'casts' must be either 'none' or an array of dictionaries in the form (from: type, check (optional): none or casted value => bool, with (optional when 'from' is dictionary): constructor => casted value => your type)." + casts-hint
|
||||||
|
)
|
||||||
|
assert(fold == none or fold == auto or type(fold) == function, message: "elembic: types.declare: 'fold' must be 'none' (no folding), 'auto' (fold each field individually) or a function 'default constructor => auto (same as (a, b) => a + b but more efficient) or function (outer, inner) => combined value'.")
|
||||||
|
|
||||||
|
let type-args = (
|
||||||
|
name: name,
|
||||||
|
fields: fields,
|
||||||
|
prefix: prefix,
|
||||||
|
doc: doc,
|
||||||
|
default: default,
|
||||||
|
parse-args: parse-args,
|
||||||
|
typecheck: typecheck,
|
||||||
|
allow-unknown-fields: allow-unknown-fields,
|
||||||
|
construct: construct,
|
||||||
|
scope: scope,
|
||||||
|
casts: casts,
|
||||||
|
fold: fold,
|
||||||
|
)
|
||||||
|
|
||||||
|
let tid = base.unique-id("t", prefix, name)
|
||||||
|
let fields = field-internals.parse-fields(fields, allow-unknown-fields: allow-unknown-fields)
|
||||||
|
let (all-fields, user-fields, foldable-fields) = fields
|
||||||
|
let auto-fold = if fold == auto { auto-fold(foldable-fields) } else { none }
|
||||||
|
|
||||||
|
let default-arg-parser = field-internals.generate-arg-parser(
|
||||||
|
fields: fields,
|
||||||
|
general-error-prefix: "elembic: type '" + name + "': ",
|
||||||
|
field-error-prefix: field-name => "field '" + field-name + "' of type '" + name + "': ",
|
||||||
|
typecheck: typecheck
|
||||||
|
)
|
||||||
|
|
||||||
|
let parse-args = if parse-args == auto {
|
||||||
|
default-arg-parser
|
||||||
|
} else {
|
||||||
|
let parse-args = parse-args(default-arg-parser, fields: fields, typecheck: typecheck)
|
||||||
|
if type(parse-args) != function {
|
||||||
|
assert(false, message: "elembic: types.declare: 'parse-args', when specified as a function, receives the default arg parser alongside `fields: fields dictionary` and `typecheck: bool`, and must return a function (the new arg parser), and not " + base.typename(parse-args))
|
||||||
|
}
|
||||||
|
|
||||||
|
parse-args
|
||||||
|
}
|
||||||
|
|
||||||
|
let default-fields = fields.user-fields.values().map(f => if f.required { (:) } else { ((f.name): f.default) }).sum(default: (:))
|
||||||
|
|
||||||
|
let typeid = (tid: tid, name: name)
|
||||||
|
|
||||||
|
// We will specify default in a bit, once we declare the constructor
|
||||||
|
let typeinfo = (
|
||||||
|
..base.base-typeinfo,
|
||||||
|
type-kind: "custom",
|
||||||
|
name: name,
|
||||||
|
input: (typeid,),
|
||||||
|
output: (typeid,),
|
||||||
|
data: (
|
||||||
|
id: typeid,
|
||||||
|
|
||||||
|
// Original type before adding casts
|
||||||
|
// or none if this is already the type before casts
|
||||||
|
// (used for 'exact()')
|
||||||
|
pre-casts: none
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
let type-data = (
|
||||||
|
(custom-type-data-key): true,
|
||||||
|
(custom-type-key): (
|
||||||
|
data-kind: "type-instance",
|
||||||
|
fields: (
|
||||||
|
version: custom-type-version,
|
||||||
|
tid: tid,
|
||||||
|
id: typeid,
|
||||||
|
),
|
||||||
|
func: declare,
|
||||||
|
default-constructor: declare,
|
||||||
|
tid: "b_custom type",
|
||||||
|
id: "custom type",
|
||||||
|
fields-known: true,
|
||||||
|
valid: true
|
||||||
|
),
|
||||||
|
version: custom-type-version,
|
||||||
|
name: name,
|
||||||
|
doc: doc,
|
||||||
|
tid: tid,
|
||||||
|
id: typeid,
|
||||||
|
// We will add this here once the constructor is declared
|
||||||
|
typeinfo: none,
|
||||||
|
scope: scope,
|
||||||
|
parse-args: parse-args,
|
||||||
|
default-fields: default-fields,
|
||||||
|
user-fields: user-fields,
|
||||||
|
all-fields: all-fields,
|
||||||
|
fields: fields,
|
||||||
|
typecheck: typecheck,
|
||||||
|
allow-unknown-fields: allow-unknown-fields,
|
||||||
|
default-constructor: none,
|
||||||
|
func: none,
|
||||||
|
type-args: type-args,
|
||||||
|
)
|
||||||
|
|
||||||
|
let process-casts = if casts == none {
|
||||||
|
none
|
||||||
|
} else {
|
||||||
|
// Trick: We assign cast to each cast-from type and create a union,
|
||||||
|
// and use its generated check/cast functions as our own
|
||||||
|
default-constructor => {
|
||||||
|
let typeinfos = casts.map(cast => {
|
||||||
|
let (res, from) = types.validate(cast.from)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.declare: invalid cast-from type: " + from)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (cast-check, with, cast-error) = if "with" in cast {
|
||||||
|
(cast.at("check", default: none), (cast.with)(default-constructor), none)
|
||||||
|
} else if from.type-kind == "native" and from.data == dictionary {
|
||||||
|
assert(fields.required-pos-fields == (), message: "elembic: types.declare: cannot generate automatic cast from dict when there are required positional fields.")
|
||||||
|
|
||||||
|
if "check" in cast {
|
||||||
|
(
|
||||||
|
cast.check,
|
||||||
|
auto-cast(dictionary, fields: fields, constructor: default-constructor),
|
||||||
|
none,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
auto-cast-check(dictionary, fields: fields, parse-args: parse-args),
|
||||||
|
auto-cast(dictionary, fields: fields, constructor: default-constructor),
|
||||||
|
auto-cast-error(dictionary, fields: fields, parse-args: parse-args),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "elembic: types.declare: cast 'with' can only be omitted for 'from: dictionary'. It must receive the default constructor and return a function 'casted value => your type'."
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(with) != function {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "elembic: types.declare: cast 'with' must receive the default constructor and return a function 'casted value => your type'. Received " + base.typename(with)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let from-cast = from.cast
|
||||||
|
|
||||||
|
types.wrap(
|
||||||
|
from,
|
||||||
|
check: from-check => if from-check == none {
|
||||||
|
if cast-check == none {
|
||||||
|
none
|
||||||
|
} else if from.cast == none {
|
||||||
|
cast-check
|
||||||
|
} else {
|
||||||
|
value => cast-check(from-cast(value))
|
||||||
|
}
|
||||||
|
} else if cast-check == none {
|
||||||
|
from-check
|
||||||
|
} else if from.cast == none {
|
||||||
|
value => from-check(value) and cast-check(value)
|
||||||
|
} else {
|
||||||
|
value => from-check(value) and cast-check(from-cast(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
output: (typeid,),
|
||||||
|
|
||||||
|
cast: from-cast => if from-cast == none {
|
||||||
|
with
|
||||||
|
} else {
|
||||||
|
value => with(from-cast(value))
|
||||||
|
},
|
||||||
|
|
||||||
|
default: (),
|
||||||
|
fold: none,
|
||||||
|
..if cast-error == none { (:) } else { (
|
||||||
|
error: if "check" not in from or from.check == none {
|
||||||
|
_ => cast-error
|
||||||
|
} else {
|
||||||
|
from-error => value => if from-error == none or from-check(value) { cast-error(value) } else { from-error(value) }
|
||||||
|
},
|
||||||
|
) }
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Accept our own typeinfo first and foremost
|
||||||
|
let union = base.union((typeinfo,) + typeinfos)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
union.output == (typeid,) and union.default == () and union.fold == none,
|
||||||
|
message: "elembic: types.declare: internal error: cast generated invalid union: " + repr(union)
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
input: union.input,
|
||||||
|
output: union.output,
|
||||||
|
check: union.check,
|
||||||
|
cast: union.cast,
|
||||||
|
error: if union.error == none {
|
||||||
|
_ => "failed to cast to custom type '" + name + "'"
|
||||||
|
} else {
|
||||||
|
x => (union.error)(x).replace("all typechecks for union failed", "all casts to custom type '" + name + "' failed")
|
||||||
|
},
|
||||||
|
data: typeinfo.data + (pre-casts: typeinfo)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let default-constructor(..args, __elembic_data: none, __elembic_func: auto) = {
|
||||||
|
if __elembic_func == auto {
|
||||||
|
__elembic_func = default-constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
let default-constructor = default-constructor.with(__elembic_func: __elembic_func)
|
||||||
|
if __elembic_data != none {
|
||||||
|
return if __elembic_data == special-data-values.get-data {
|
||||||
|
let typeinfo = typeinfo + if process-casts != none { process-casts(default-constructor) } else { (:) }
|
||||||
|
if default != none {
|
||||||
|
typeinfo.default = (default(default-constructor),)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auto-fold != none {
|
||||||
|
typeinfo.fold = auto-fold
|
||||||
|
} else if type(fold) == function {
|
||||||
|
let fold = fold(default-constructor)
|
||||||
|
if fold != auto and type(fold) != function {
|
||||||
|
assert(false, message: "elembic: types: custom type did not specify a valid fold, must be a function default constructor => value, got " + base.typename(fold))
|
||||||
|
}
|
||||||
|
typeinfo.fold = fold
|
||||||
|
}
|
||||||
|
|
||||||
|
(data-kind: "custom-type-data", ..type-data, typeinfo: typeinfo, func: __elembic_func, default-constructor: default-constructor)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (res, args) = parse-args(args, include-required: true)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: args)
|
||||||
|
}
|
||||||
|
|
||||||
|
let final-fields = default-fields + args
|
||||||
|
|
||||||
|
if foldable-fields != (:) {
|
||||||
|
// Fold received arguments with defaults
|
||||||
|
for (field-name, fold-data) in foldable-fields {
|
||||||
|
if field-name in args {
|
||||||
|
let outer = default-fields.at(field-name, default: fold-data.default)
|
||||||
|
if fold-data.folder == auto {
|
||||||
|
final-fields.at(field-name) = outer + args.at(field-name)
|
||||||
|
} else {
|
||||||
|
final-fields.at(field-name) = (fold-data.folder)(outer, args.at(field-name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final-fields.insert(
|
||||||
|
custom-type-key,
|
||||||
|
(
|
||||||
|
data-kind: "type-instance",
|
||||||
|
fields: final-fields,
|
||||||
|
func: __elembic_func,
|
||||||
|
default-constructor: default-constructor,
|
||||||
|
tid: tid,
|
||||||
|
id: (tid: tid, name: name),
|
||||||
|
scope: scope,
|
||||||
|
fields-known: true,
|
||||||
|
valid: true
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
final-fields
|
||||||
|
}
|
||||||
|
|
||||||
|
default = if default == none {
|
||||||
|
()
|
||||||
|
} else {
|
||||||
|
let default = default(default-constructor)
|
||||||
|
assert(
|
||||||
|
type(default) == dictionary and custom-type-key in default and default.at(custom-type-key).id == typeid,
|
||||||
|
message: "elembic: types.declare: the 'default' function must return an instance of the new type using the provided constructor, not " + repr(default)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
(default,)
|
||||||
|
}
|
||||||
|
|
||||||
|
fold = if auto-fold != none {
|
||||||
|
auto-fold
|
||||||
|
} else if type(fold) == function {
|
||||||
|
let fold = fold(default-constructor)
|
||||||
|
if fold != auto and type(fold) != function {
|
||||||
|
assert(false, message: "elembic: types.declare: a valid fold was not specified, must be a function default constructor => value, got " + base.typename(fold))
|
||||||
|
}
|
||||||
|
fold
|
||||||
|
} else {
|
||||||
|
none
|
||||||
|
}
|
||||||
|
|
||||||
|
if process-casts != none {
|
||||||
|
typeinfo += process-casts(default-constructor)
|
||||||
|
}
|
||||||
|
typeinfo.default = default
|
||||||
|
typeinfo.fold = fold
|
||||||
|
type-data.typeinfo = typeinfo
|
||||||
|
|
||||||
|
let final-constructor = if construct != none {
|
||||||
|
{
|
||||||
|
let test-construct = construct(default-constructor)
|
||||||
|
assert(type(test-construct) == function, message: "elembic: types.declare: the 'construct' function must receive the default constructor and return the new constructor, a new function, not '" + str(type(test-construct)) + "'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
let final-constructor(..args, __elembic_data: none) = {
|
||||||
|
if __elembic_data != none {
|
||||||
|
return if __elembic_data == special-data-values.get-data {
|
||||||
|
(data-kind: "custom-type-data", ..type-data, func: final-constructor, default-constructor: default-constructor.with(__elembic_func: final-constructor))
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types: invalid data key to constructor: " + repr(__elembic_data))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
construct(default-constructor.with(__elembic_func: final-constructor))(..args)
|
||||||
|
}
|
||||||
|
|
||||||
|
final-constructor
|
||||||
|
} else {
|
||||||
|
default-constructor
|
||||||
|
}
|
||||||
|
|
||||||
|
type-data.default-constructor = default-constructor.with(__elembic_func: final-constructor)
|
||||||
|
type-data.func = final-constructor
|
||||||
|
|
||||||
|
final-constructor
|
||||||
|
}
|
||||||
349
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/native.typ
vendored
Normal file
349
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/native.typ
vendored
Normal file
@@ -0,0 +1,349 @@
|
|||||||
|
// Typst-native types.
|
||||||
|
#import "../data.typ": type-key
|
||||||
|
#import "base.typ": base-typeinfo, ok, err
|
||||||
|
|
||||||
|
// Tiling type (renamed in Typst 0.13.0)
|
||||||
|
#let tiling = if sys.version < version(0, 13, 0) { pattern } else { tiling }
|
||||||
|
|
||||||
|
#let native-base = (
|
||||||
|
..base-typeinfo,
|
||||||
|
type-kind: "native",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Generic typeinfo for a native type.
|
||||||
|
// PROPERTY: if type key is native, then output has the native type,
|
||||||
|
// and input has a list of native types that can be cast to it.
|
||||||
|
#let generic-typeinfo(native-type) = {
|
||||||
|
assert(type(native-type) == type(str), message: "elembic: internal error: not a type")
|
||||||
|
|
||||||
|
(
|
||||||
|
..native-base,
|
||||||
|
name: str(native-type),
|
||||||
|
input: (native-type,),
|
||||||
|
output: (native-type,),
|
||||||
|
data: native-type,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Castable types
|
||||||
|
|
||||||
|
#let content_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(content),
|
||||||
|
input: (type(none), content, str, symbol),
|
||||||
|
output: (content,),
|
||||||
|
data: content,
|
||||||
|
cast: x => [#x],
|
||||||
|
default: ([],),
|
||||||
|
)
|
||||||
|
#let float_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(float),
|
||||||
|
input: (float, int),
|
||||||
|
output: (float,),
|
||||||
|
data: float,
|
||||||
|
cast: float,
|
||||||
|
default: (0.0,),
|
||||||
|
)
|
||||||
|
#let stroke-keys = ("paint", "thickness", "cap", "join", "dash", "miter-limit")
|
||||||
|
#let stroke_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(stroke),
|
||||||
|
input: (stroke, length, color, gradient, tiling, dictionary),
|
||||||
|
output: (stroke,),
|
||||||
|
data: stroke,
|
||||||
|
cast: stroke,
|
||||||
|
check: v => type(v) != dictionary or v.keys().all(k => k in stroke-keys),
|
||||||
|
default: (stroke(),),
|
||||||
|
// Allow specifying e.g. 4pt in one set rule, red in the other => 4pt + red in the end
|
||||||
|
fold: (outer, inner) => {
|
||||||
|
// Can't sum stroke with stroke, so can't optimize with 'fold: auto' :(
|
||||||
|
stroke(
|
||||||
|
paint: if inner.paint == auto { outer.paint } else { inner.paint },
|
||||||
|
thickness: if inner.thickness == auto { outer.thickness } else { inner.thickness },
|
||||||
|
cap: if inner.cap == auto { outer.cap } else { inner.cap },
|
||||||
|
join: if inner.join == auto { outer.join } else { inner.join },
|
||||||
|
dash: if inner.dash == auto { outer.dash } else { inner.dash },
|
||||||
|
miter-limit: if inner.miter-limit == auto { outer.miter-limit } else { inner.miter-limit },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
#let relative_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(relative),
|
||||||
|
input: (relative, length, ratio),
|
||||||
|
output: (relative,),
|
||||||
|
data: relative,
|
||||||
|
cast: x => x + 0% + 0pt,
|
||||||
|
default: (0% + 0pt,),
|
||||||
|
)
|
||||||
|
#let function_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(function),
|
||||||
|
// Would add symbol as well, but missing a reliable way to check for callable symbols
|
||||||
|
input: (type, function),
|
||||||
|
output: (type, function,),
|
||||||
|
data: function,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Folding types (also includes stroke above)
|
||||||
|
#let array_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(array),
|
||||||
|
input: (array,),
|
||||||
|
output: (array,),
|
||||||
|
data: array,
|
||||||
|
default: ((),),
|
||||||
|
|
||||||
|
// Array fields are joined together by default:
|
||||||
|
// set(field: (1, 2)), set(field: (3, 4))
|
||||||
|
// => set(field: (1, 2, 3, 4))
|
||||||
|
fold: auto,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let dict_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(dictionary),
|
||||||
|
input: (dictionary,),
|
||||||
|
output: (dictionary,),
|
||||||
|
data: dictionary,
|
||||||
|
default: ((:),),
|
||||||
|
|
||||||
|
// Dictionary fields are joined together by default:
|
||||||
|
// set(field: (a: 12)), set(field: (b: 13))
|
||||||
|
// => set(field: (a: 12, b: 13))
|
||||||
|
fold: auto,
|
||||||
|
)
|
||||||
|
|
||||||
|
#let alignment_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(alignment),
|
||||||
|
input: (alignment,),
|
||||||
|
output: (alignment,),
|
||||||
|
data: alignment,
|
||||||
|
fold: (outer, inner) => if inner.axis() == none or outer.axis() == inner.axis() {
|
||||||
|
// If axis A == axis B, we override. For example, left -> right. (No sum)
|
||||||
|
// Same if both are none (2D alignments), in which case inner fully overrides as well (left + top -> center + bottom).
|
||||||
|
// In addition, if inner axis is none (it is a 2D alignment), it overrides in both ways (left -> right + top).
|
||||||
|
inner
|
||||||
|
} else if outer.axis() == none {
|
||||||
|
// Here, we know that inner isn't 2D, so either outer is 2D or both have different axes.
|
||||||
|
// If outer is 2D and inner is 1D, inner replaces its axis in outer, but the other axis is kept.
|
||||||
|
if inner.axis() == "horizontal" {
|
||||||
|
inner + outer.y
|
||||||
|
} else {
|
||||||
|
outer.x + inner
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Both are 1D and have distinct axes, so we just sum.
|
||||||
|
// left and top => left + top
|
||||||
|
// bottom and right => right + bottom
|
||||||
|
inner + outer
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Simple types (no casting)
|
||||||
|
|
||||||
|
#let str_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(str),
|
||||||
|
input: (str,),
|
||||||
|
output: (str,),
|
||||||
|
data: str,
|
||||||
|
default: ("",)
|
||||||
|
)
|
||||||
|
#let bool_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(bool),
|
||||||
|
input: (bool,),
|
||||||
|
output: (bool,),
|
||||||
|
data: bool,
|
||||||
|
default: (false,)
|
||||||
|
)
|
||||||
|
#let int_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(int),
|
||||||
|
input: (int,),
|
||||||
|
output: (int,),
|
||||||
|
data: int,
|
||||||
|
default: (0,),
|
||||||
|
)
|
||||||
|
#let color_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(color),
|
||||||
|
input: (color,),
|
||||||
|
output: (color,),
|
||||||
|
data: color,
|
||||||
|
)
|
||||||
|
#let gradient_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(gradient),
|
||||||
|
input: (gradient,),
|
||||||
|
output: (gradient,),
|
||||||
|
data: gradient,
|
||||||
|
)
|
||||||
|
#let tiling_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(tiling),
|
||||||
|
input: (tiling,),
|
||||||
|
output: (tiling,),
|
||||||
|
data: tiling,
|
||||||
|
)
|
||||||
|
#let datetime_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(datetime),
|
||||||
|
input: (datetime,),
|
||||||
|
output: (datetime,),
|
||||||
|
data: datetime,
|
||||||
|
)
|
||||||
|
#let angle_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(angle),
|
||||||
|
input: (angle,),
|
||||||
|
output: (angle,),
|
||||||
|
data: angle,
|
||||||
|
default: (0deg,),
|
||||||
|
)
|
||||||
|
#let ratio_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(ratio),
|
||||||
|
input: (ratio,),
|
||||||
|
output: (ratio,),
|
||||||
|
data: ratio,
|
||||||
|
default: (0%,),
|
||||||
|
)
|
||||||
|
#let length_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(length),
|
||||||
|
input: (length,),
|
||||||
|
output: (length,),
|
||||||
|
data: length,
|
||||||
|
default: (0pt,),
|
||||||
|
)
|
||||||
|
#let fraction_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(fraction),
|
||||||
|
input: (fraction,),
|
||||||
|
output: (fraction,),
|
||||||
|
data: fraction,
|
||||||
|
default: (0fr,),
|
||||||
|
)
|
||||||
|
#let duration_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(duration),
|
||||||
|
input: (duration,),
|
||||||
|
output: (duration,),
|
||||||
|
data: duration,
|
||||||
|
default: (duration(seconds: 0),),
|
||||||
|
)
|
||||||
|
#let type_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(type),
|
||||||
|
input: (type,),
|
||||||
|
output: (type,),
|
||||||
|
data: type,
|
||||||
|
)
|
||||||
|
#let arguments_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(arguments),
|
||||||
|
input: (arguments,),
|
||||||
|
output: (arguments,),
|
||||||
|
data: arguments,
|
||||||
|
default: (arguments(),),
|
||||||
|
)
|
||||||
|
#let bytes_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(bytes),
|
||||||
|
input: (bytes,),
|
||||||
|
output: (bytes,),
|
||||||
|
data: bytes,
|
||||||
|
default: (bytes(()),),
|
||||||
|
)
|
||||||
|
#let version_ = (
|
||||||
|
..native-base,
|
||||||
|
name: str(version),
|
||||||
|
input: (version,),
|
||||||
|
output: (version,),
|
||||||
|
data: version,
|
||||||
|
default: (version(0, 0, 0),),
|
||||||
|
)
|
||||||
|
|
||||||
|
// None / auto
|
||||||
|
|
||||||
|
#let none_ = (
|
||||||
|
..native-base,
|
||||||
|
name: "none",
|
||||||
|
input: (type(none),),
|
||||||
|
output: (type(none),),
|
||||||
|
data: type(none),
|
||||||
|
default: (none,)
|
||||||
|
)
|
||||||
|
#let auto_ = (
|
||||||
|
..native-base,
|
||||||
|
name: "auto",
|
||||||
|
input: (type(auto),),
|
||||||
|
output: (type(auto),),
|
||||||
|
data: type(auto),
|
||||||
|
default: (auto,)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Return the typeinfo for a native type.
|
||||||
|
#let typeinfo(t) = {
|
||||||
|
let out = if t == content {
|
||||||
|
content_
|
||||||
|
} else if t == int {
|
||||||
|
int_
|
||||||
|
} else if t == bool {
|
||||||
|
bool_
|
||||||
|
} else if t == float {
|
||||||
|
float_
|
||||||
|
} else if t == type(none) {
|
||||||
|
none_
|
||||||
|
} else if t == type(auto) {
|
||||||
|
auto_
|
||||||
|
} else if t == dictionary {
|
||||||
|
dict_
|
||||||
|
} else if t == array {
|
||||||
|
array_
|
||||||
|
} else if t == str {
|
||||||
|
str_
|
||||||
|
} else if t == color {
|
||||||
|
color_
|
||||||
|
} else if t == gradient {
|
||||||
|
gradient_
|
||||||
|
} else if t == datetime {
|
||||||
|
datetime_
|
||||||
|
} else if t == duration {
|
||||||
|
duration_
|
||||||
|
} else if t == function {
|
||||||
|
function_
|
||||||
|
} else if t == relative {
|
||||||
|
relative_
|
||||||
|
} else if t == stroke {
|
||||||
|
stroke_
|
||||||
|
} else if t == tiling {
|
||||||
|
tiling_
|
||||||
|
} else if t == type {
|
||||||
|
type_
|
||||||
|
} else if t == angle {
|
||||||
|
angle_
|
||||||
|
} else if t == alignment {
|
||||||
|
alignment_
|
||||||
|
} else if t == ratio {
|
||||||
|
ratio_
|
||||||
|
} else if t == length {
|
||||||
|
length_
|
||||||
|
} else if t == fraction {
|
||||||
|
fraction_
|
||||||
|
} else if t == arguments {
|
||||||
|
arguments_
|
||||||
|
} else if t == bytes {
|
||||||
|
bytes_
|
||||||
|
} else if t == version {
|
||||||
|
version_
|
||||||
|
} else {
|
||||||
|
generic-typeinfo(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
(true, out)
|
||||||
|
}
|
||||||
434
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/types.typ
vendored
Normal file
434
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/src/types/types.typ
vendored
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
// The type system used by fields.
|
||||||
|
#import "../data.typ": data, special-data-values, type-key, custom-type-key, custom-type-data-key, eq, type-version, elem-funcs
|
||||||
|
#import "base.typ" as base: ok, err
|
||||||
|
#import "native.typ"
|
||||||
|
|
||||||
|
// The default value for a type.
|
||||||
|
#let default(type_) = {
|
||||||
|
if type_.default == () {
|
||||||
|
let prefix = if type_.type-kind in ("native", "union") { type_.type-kind + " " } else { "" }
|
||||||
|
err(prefix + "type '" + type_.name + "' has no known default, please specify an explicit 'default: value' or set 'required: true' for the field")
|
||||||
|
} else {
|
||||||
|
ok(type_.default.first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#let typeof(value) = {
|
||||||
|
let element-data
|
||||||
|
if type(value) == dictionary and custom-type-key in value {
|
||||||
|
if custom-type-data-key in value {
|
||||||
|
base.custom-type
|
||||||
|
} else {
|
||||||
|
(value.at(custom-type-key).func)(__elembic_data: special-data-values.get-data).typeinfo
|
||||||
|
}
|
||||||
|
} else if type(value) == content and value.func() in elem-funcs and {
|
||||||
|
element-data = data(value)
|
||||||
|
element-data.eid != none
|
||||||
|
} {
|
||||||
|
if "name" in element-data and type(element-data.name) == str {
|
||||||
|
base.element(element-data.name, element-data.eid)
|
||||||
|
} else {
|
||||||
|
base.element("unknown-element", element-data.eid)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let (res, typeinfo) = native.typeinfo(type(value))
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.typeof: " + typeinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
typeinfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Literal type
|
||||||
|
// Only accepted if value is equal to the literal.
|
||||||
|
// Input and output are equal to the value.
|
||||||
|
//
|
||||||
|
// Uses base typeinfo information for information such as casts and whatnot.
|
||||||
|
#let literal(value) = {
|
||||||
|
if value == none {
|
||||||
|
native.none_
|
||||||
|
} else if value == auto {
|
||||||
|
native.auto_
|
||||||
|
} else {
|
||||||
|
base.literal(value, typeof(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtain the typeinfo for a type.
|
||||||
|
//
|
||||||
|
// Returns ok(typeinfo), or err(error) if there is no corresponding typeinfo.
|
||||||
|
#let validate(type_) = {
|
||||||
|
if type(type_) == function {
|
||||||
|
let data = type_(__elembic_data: special-data-values.get-data)
|
||||||
|
let data-kind = data.at("data-kind", default: "unknown")
|
||||||
|
if data-kind == "custom-type-data" {
|
||||||
|
type_ = data.typeinfo
|
||||||
|
} else if data-kind == "element" {
|
||||||
|
type_ = base.element(data.name, data.eid)
|
||||||
|
} else {
|
||||||
|
return (false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(type_) == type {
|
||||||
|
native.typeinfo(type_)
|
||||||
|
} else if type(type_) == dictionary and type-key in type_ {
|
||||||
|
(true, type_)
|
||||||
|
} else if type(type_) == dictionary and custom-type-data-key in type_ {
|
||||||
|
(true, type_.typeinfo)
|
||||||
|
} else if type(type_) == function {
|
||||||
|
(false, "A function is not a valid type. (You can use 'types.literal(func)' to only accept a particular function.)")
|
||||||
|
} else if type_ == none or type_ == auto {
|
||||||
|
// Accept none or auto to mean their types
|
||||||
|
native.typeinfo(type(type_))
|
||||||
|
} else if type(type_) not in (dictionary, array, content) {
|
||||||
|
// Automatically accept literals
|
||||||
|
(true, literal(type_))
|
||||||
|
} else {
|
||||||
|
(false, "Received invalid type: " + repr(type_) + "\n hint: use 'types.literal(value)' to indicate only that particular value is valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error when a value doesn't conform to a certain cast
|
||||||
|
#let generate-cast-error(value, typeinfo, hint: none) = {
|
||||||
|
let message = if "any" not in typeinfo.input and base.typeid(value) not in typeinfo.input {
|
||||||
|
if typeinfo.input == () {
|
||||||
|
"type '" + typeinfo.name + "' does not accept any values"
|
||||||
|
} else {
|
||||||
|
(
|
||||||
|
"expected "
|
||||||
|
+ typeinfo.input.map(t => if type(t) == dictionary and "name" in t { t.name } else { str(t) }).join(", ", last: " or ")
|
||||||
|
+ ", found "
|
||||||
|
+ base.typename(value)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else if typeinfo.at("error", default: none) != none {
|
||||||
|
(typeinfo.error)(value)
|
||||||
|
} else {
|
||||||
|
"typecheck for " + typeinfo.name + " failed"
|
||||||
|
}
|
||||||
|
let given-hint = if hint == none { "" } else { "\n hint: " + hint }
|
||||||
|
|
||||||
|
message + given-hint
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try to accept value via given typeinfo or return error
|
||||||
|
// Returns ok(value) a.k.a. (true, value) on success
|
||||||
|
// Returns err(value) a.k.a. (false, value) on error
|
||||||
|
#let cast(value, typeinfo) = {
|
||||||
|
if type(typeinfo) != dictionary or type-key not in typeinfo {
|
||||||
|
let (res, typeinfo-or-err) = validate(typeinfo)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.cast: " + typeinfo-or-err)
|
||||||
|
}
|
||||||
|
typeinfo = typeinfo-or-err
|
||||||
|
}
|
||||||
|
|
||||||
|
let kind = typeinfo.type-kind
|
||||||
|
if kind == "any" {
|
||||||
|
(true, value)
|
||||||
|
} else {
|
||||||
|
let value-type = type(value)
|
||||||
|
if value-type == dictionary and custom-type-key in value {
|
||||||
|
value-type = value.at(custom-type-key).id
|
||||||
|
}
|
||||||
|
|
||||||
|
if kind == "literal" and typeinfo.cast == none and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) {
|
||||||
|
if eq(value, typeinfo.data.value) and (value-type in typeinfo.input or "any" in typeinfo.input) and (typeinfo.data.typeinfo.check == none or (typeinfo.data.typeinfo.check)(value)) {
|
||||||
|
(true, value)
|
||||||
|
} else {
|
||||||
|
(false, generate-cast-error(value, typeinfo))
|
||||||
|
}
|
||||||
|
} else if (
|
||||||
|
value-type not in typeinfo.input and "any" not in typeinfo.input
|
||||||
|
or typeinfo.check != none and not (typeinfo.check)(value)
|
||||||
|
) {
|
||||||
|
(false, generate-cast-error(value, typeinfo))
|
||||||
|
} else if typeinfo.cast == none {
|
||||||
|
(true, value)
|
||||||
|
} else if kind == "native" and typeinfo.data == content and ("__future_cast" not in typeinfo or typeinfo.__future_cast.max-version < type-version) {
|
||||||
|
(true, [#value])
|
||||||
|
} else {
|
||||||
|
(true, (typeinfo.cast)(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Expected types for each typeinfo key.
|
||||||
|
#let overridable-typeinfo-types = (
|
||||||
|
name: (check: a => type(a) == str, error: "string or function old name => new name"),
|
||||||
|
input: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old input => new input"),
|
||||||
|
output: (check: a => type(a) == array and a.all(x => x == "any" or x == "custom type" or type(x) == type or (type(x) == dictionary and "tid" in x)), error: "array of \"any\", \"custom type\", type, or custom type id (tid: ...), or function old output => new output"),
|
||||||
|
check: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function value => bool"),
|
||||||
|
cast: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => output"),
|
||||||
|
error: (check: a => a == none or type(a) == function, error: "none or function receiving old function and returning a function checked input => error string"),
|
||||||
|
default: (check: d => d == () or type(d) == array and d.len() == 1, error: "empty array for no default, singleton array for one default, or function old default => new default"),
|
||||||
|
fold: (check: f => f == none or f == auto or type(f) == function, error: "none for no folding, auto to fold with sum (same as (a, b) => a + b), or function receiving old fold and returning either none or auto, or a new function (outer, inner) => combined value"),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Wrap a type, altering its properties while keeping (or replacing) its input types and checks.
|
||||||
|
#let wrap(type_, ..data) = {
|
||||||
|
assert(data.pos() == (), message: "elembic: types.wrap: unexpected positional arguments")
|
||||||
|
let (res, typeinfo) = validate(type_)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.wrap: " + typeinfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
let overrides = data.named()
|
||||||
|
for (key, value) in overrides {
|
||||||
|
let (check: validate-value, error: key-error) = overridable-typeinfo-types.at(key, default: (check: none, error: none))
|
||||||
|
if validate-value == none or key-error == none {
|
||||||
|
assert(false, message: "elembic: types.wrap: invalid key '" + key + "', must be one of " + overridable-typeinfo-types.keys().join(", ", last: " or "))
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(value) == function {
|
||||||
|
value = value(typeinfo.at(key, default: base.base-typeinfo.at(key)))
|
||||||
|
overrides.at(key) = value
|
||||||
|
}
|
||||||
|
|
||||||
|
if not validate-value(value) {
|
||||||
|
let array-hint = if key in ("input", "output", "default") and type(value) != array {
|
||||||
|
"\n hint: did you forget a comma at (value,) and wrote (value) instead? Make sure an array was given."
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(false, message: "elembic: types.wrap: invalid value for key '" + key + "', expected " + key-error + array-hint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if "any" not in typeinfo.output and "cast" in overrides and "output" not in overrides {
|
||||||
|
// - If there is a cast and output is unchanged, then complain: please update the output
|
||||||
|
assert(false, message: "elembic: types.wrap: please override 'output' whenever overriding 'cast', specifying which types the cast function may return, or '(\"any\",)' (note the comma!) to indicate the cast function may return any type (discouraged, disables some optimizations).\n\nYou can also tell elembic you are sure the new cast function may produce strictly the same output types as before with 'output: prev => prev'.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if typeinfo.cast != none and "output" in overrides and "cast" not in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output) {
|
||||||
|
// If output was changed to a list which isn't 'any' and isn't a superset of the previous output,
|
||||||
|
// then ensure casting is also changed, as it is no longer safe (might produce something that is
|
||||||
|
// an invalid output)
|
||||||
|
assert(false, message: "elembic: types.wrap: a type output was removed, but its cast was not changed, meaning the cast function might produce a now invalid output. Fix this by either providing a new cast function, setting 'cast: none' to disable casting entirely, or setting 'cast: prev => prev' if you're sure the previous cast function cannot produce one of the removed output types.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if "output" in overrides and "any" in overrides.output {
|
||||||
|
// - Collapse "any" + other types into just "any"
|
||||||
|
overrides.output = ("any",)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "input" in overrides and "any" in overrides.input {
|
||||||
|
// - Collapse "any" + other types into just "any"
|
||||||
|
overrides.input = ("any",)
|
||||||
|
}
|
||||||
|
|
||||||
|
if "default" not in overrides and typeinfo.default != () and ("check" in overrides or "output" in overrides and "any" not in overrides.output and typeinfo.output.any(o => o not in overrides.output)) {
|
||||||
|
// Not sure if default would fit those criteria anymore:
|
||||||
|
// 1. By overriding the check, it's possible that a type such as positive int (check: int > 0) would no longer
|
||||||
|
// have an acceptable default when changing its check to, say, negative int (check: int < 0).
|
||||||
|
// 2. By overriding the output and removing previous output types, it's possible the default no longer has a valid type (it must be a valid output).
|
||||||
|
overrides.default = ()
|
||||||
|
}
|
||||||
|
|
||||||
|
let new-default = overrides.at("default", default: typeinfo.default)
|
||||||
|
let new-output = overrides.at("output", default: typeinfo.output)
|
||||||
|
let new-input = overrides.at("input", default: typeinfo.input)
|
||||||
|
let new-cast = overrides.at("cast", default: typeinfo.cast)
|
||||||
|
let new-check = overrides.at("check", default: typeinfo.check)
|
||||||
|
assert(
|
||||||
|
new-default == ()
|
||||||
|
or "any" in new-output
|
||||||
|
or base.typeid(new-default.first()) in new-output,
|
||||||
|
|
||||||
|
message: "elembic: types.wrap: new default (currently " + repr(if new-default == () { none } else { new-default.first() }) + ") must have a type within possible 'output' types of the new type (currently " + if new-output == () { "empty" } else { new-output.map(t => if type(t) == dictionary { t.name } else { str(t) }).join(", ", last: " or ") } + "), since it is itself an output\n hint: you can either change the default, or update possible output types with 'output: (new, list)' to indicate which native or custom types your wrapped type might end up as after casts (if there are casts)."
|
||||||
|
)
|
||||||
|
|
||||||
|
if new-check == none and new-cast == none and "any" not in new-output and (
|
||||||
|
"any" in new-input or new-input.any(inp => inp not in new-output)
|
||||||
|
) {
|
||||||
|
assert(false, message: "elembic: types.wrap: new type has no casting or checking, but not all of its input types are valid output types. Ensure 'output: (...)' and 'input: (...)' are identical to fix this.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if new-cast == none and "any" not in new-input and new-output.any(out => out == "any" or out not in new-input) {
|
||||||
|
assert(false, message: "elembic: types.wrap: new type has no casting, but list of valid output types includes invalid input types. Please ensure 'output' is a subset of 'input' in this case, or add a cast.")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(
|
||||||
|
"check" in overrides and new-check != typeinfo.check
|
||||||
|
or "output" in overrides and new-output != typeinfo.output
|
||||||
|
)
|
||||||
|
and "fold" not in overrides and typeinfo.fold != none
|
||||||
|
) {
|
||||||
|
// Folding might not be valid anymore:
|
||||||
|
// 1. By overriding the check, it's possible a fold that, say, adds two numbers, would no longer be valid
|
||||||
|
// if, for example, the new check ensures each number is smaller than 59 (you might add up to that).
|
||||||
|
// In addition, the fold might now receive parameters that would fail the new check while being cast.
|
||||||
|
// 2. By overriding the output:
|
||||||
|
// a. and removing old output, it's possible the fold produces invalid output.
|
||||||
|
// b. and adding new output, it's possible the fold receives parameters of an unexpected type.
|
||||||
|
assert(false, message: "elembic: types.wrap: new type has overridden check and/or output types, but not 'fold', usually a function (outer output, inner output) => folded output\n hint: it's possible the previous fold function could now produce a value that would never have passed the check or been a valid output type if kept, e.g. joining two single-element arrays would generate a two-element array which may violate a check that only allows casting single-element arrays into the new type\n hint: either explicitly remove folding with 'fold: none', keep the previous fold function with 'fold: prev => prev' if you're sure it still works properly with the new check or list of output types, or override it with 'fold: prev => (outer, inner) => folded'")
|
||||||
|
}
|
||||||
|
|
||||||
|
base.wrap(typeinfo, overrides)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Specifies that any from a given selection of types is accepted.
|
||||||
|
#let union(..args) = {
|
||||||
|
let types = args.pos()
|
||||||
|
assert(types != (), message: "elembic: types.union: please specify at least one type")
|
||||||
|
|
||||||
|
let typeinfos = types.map(type_ => {
|
||||||
|
let (res, typeinfo-or-err) = validate(type_)
|
||||||
|
assert(res, message: if not res { "elembic: types.union: " + typeinfo-or-err } else { "" })
|
||||||
|
|
||||||
|
typeinfo-or-err
|
||||||
|
})
|
||||||
|
|
||||||
|
base.union(typeinfos)
|
||||||
|
}
|
||||||
|
|
||||||
|
// An optional type (can be 'none').
|
||||||
|
#let option(type_) = union(type(none), type_)
|
||||||
|
|
||||||
|
// A type which can be 'auto'.
|
||||||
|
#let smart(type_) = union(type(auto), type_)
|
||||||
|
|
||||||
|
#let array_(type_) = {
|
||||||
|
let (res, param) = validate(type_)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.array: " + param)
|
||||||
|
}
|
||||||
|
|
||||||
|
base.array_(
|
||||||
|
native.array_,
|
||||||
|
param,
|
||||||
|
|
||||||
|
error: if param.check == none {
|
||||||
|
a => {
|
||||||
|
let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => {
|
||||||
|
if "any" not in param.input and base.typeid(element) not in param.input {
|
||||||
|
(count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param))
|
||||||
|
} else {
|
||||||
|
(count, message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let n-elements = if count == 1 { "an element" } else { str(count) + " elements" }
|
||||||
|
n-elements + " in an array of " + param.name + " did not typecheck" + message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
a => {
|
||||||
|
let (count, message) = a.enumerate().fold((0, ""), ((count, message), (i, element)) => {
|
||||||
|
if "any" not in param.input and base.typeid(element) not in param.input or not (param.check)(element) {
|
||||||
|
(count + 1, message + "\n hint: at position " + str(i) + ": " + generate-cast-error(element, param))
|
||||||
|
} else {
|
||||||
|
(count, message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let n-elements = if count == 1 { "an element" } else { str(count) + " elements" }
|
||||||
|
n-elements + " in an array of " + param.name + " did not typecheck" + message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let dict_(type_) = {
|
||||||
|
let (res, param) = validate(type_)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.array: " + param)
|
||||||
|
}
|
||||||
|
|
||||||
|
base.dict_(
|
||||||
|
native.dict_,
|
||||||
|
param,
|
||||||
|
|
||||||
|
error: if param.check == none {
|
||||||
|
d => {
|
||||||
|
let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => {
|
||||||
|
if "any" not in param.input and base.typeid(value) not in param.input {
|
||||||
|
(count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param))
|
||||||
|
} else {
|
||||||
|
(count, message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let n-elements = if count == 1 { "a value" } else { str(count) + " values" }
|
||||||
|
n-elements + " in a dictionary of " + param.name + " did not typecheck" + message
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
d => {
|
||||||
|
let (count, message) = d.pairs().fold((0, ""), ((count, message), (key, value)) => {
|
||||||
|
if "any" not in param.input and base.typeid(value) not in param.input or not (param.check)(value) {
|
||||||
|
(count + 1, message + "\n hint: at key " + repr(key) + ": " + generate-cast-error(value, param))
|
||||||
|
} else {
|
||||||
|
(count, message)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let n-elements = if count == 1 { "a value" } else { str(count) + " values" }
|
||||||
|
n-elements + " in a dictionary of " + param.name + " did not typecheck" + message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Native paint type. Can be used for fills, strokes and so on.
|
||||||
|
#let paint = union(color, gradient, native.tiling_)
|
||||||
|
|
||||||
|
// Force the type to only accept its outputs (disallow casting).
|
||||||
|
// Folding is kept if possible.
|
||||||
|
#let exact(type_) = {
|
||||||
|
let (res, type_) = validate(type_)
|
||||||
|
if not res {
|
||||||
|
assert(false, message: "elembic: types.exact: " + type_)
|
||||||
|
}
|
||||||
|
|
||||||
|
let key = if type(type_) == dictionary and "type-kind" in type_ { type_.type-kind } else { none }
|
||||||
|
if key == "union" {
|
||||||
|
// exact(union(A, B)) === union(exact(A), exact(B))
|
||||||
|
union(..type_.data.map(exact))
|
||||||
|
} else if type(type_) == type or key == "native" {
|
||||||
|
// exact(float) => can only pass float, not int
|
||||||
|
// exact(stroke) => can only pass stroke, not length, gradient, dict, etc.
|
||||||
|
let native-type = type_.data
|
||||||
|
(
|
||||||
|
..native.generic-typeinfo(native-type),
|
||||||
|
default: if type_.default != () and type(type_.default.first()) == native-type { type_.default } else { () },
|
||||||
|
|
||||||
|
// Fold is an output => output function. The new output will be just (native-type,),
|
||||||
|
// so if fold previously accepted that native type, it will still accept it, so it
|
||||||
|
// can be kept.
|
||||||
|
fold: if native-type in type_.output { type_.fold } else { none },
|
||||||
|
)
|
||||||
|
} else if key == "literal" {
|
||||||
|
// exact(literal) => literal with base type modified to exact(base type)
|
||||||
|
assert(type(type_.data.value) not in (dictionary, array), message: "elembic: types.exact: exact literal types for custom types, dictionaries and arrays are not supported\n hint: consider customizing the check function to recursively check fields if the performance is acceptable")
|
||||||
|
|
||||||
|
base.literal(type_.data.value, exact(type_.data.typeinfo))
|
||||||
|
} else if key == "any" or key == "never" {
|
||||||
|
// exact(any) => any (same)
|
||||||
|
// exact(never) => never (same)
|
||||||
|
type_
|
||||||
|
} else if key == "collection" {
|
||||||
|
if "base" in type_.data and "parameters" in type_.data {
|
||||||
|
let base-kind = type_.data.base.at("type-kind", default: none)
|
||||||
|
if base-kind == "native" and type_.data.base.data == array {
|
||||||
|
array_(..type_.data.parameters.map(exact))
|
||||||
|
} else if base-kind == "native" and type_.data.base.data == dictionary {
|
||||||
|
dict_(..type_.data.parameters.map(exact))
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.exact: unknown collection with type kind '" + base-kind + "'" + if base-kind == "native" { ", base native type '" + type_.data.base.name + "'" } else { "" })
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.exact: invalid collection given")
|
||||||
|
}
|
||||||
|
} else if key == "custom" {
|
||||||
|
if type_.data.pre-casts == none {
|
||||||
|
type_
|
||||||
|
} else {
|
||||||
|
type_.data.pre-casts
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(false, message: "elembic: types.exact: unsupported type kind " + key + ", supported kinds include native types, literals, custom types, arrays, dicts, 'any' and 'never'")
|
||||||
|
}
|
||||||
|
}
|
||||||
15
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/typst.toml
vendored
Normal file
15
.obsidian/plugins/typst-mate/packages/preview/elembic/1.1.1/typst.toml
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "elembic"
|
||||||
|
version = "1.1.1"
|
||||||
|
compiler = "0.11.0"
|
||||||
|
homepage = "https://pgbiel.github.io/elembic"
|
||||||
|
repository = "https://github.com/PgBiel/elembic"
|
||||||
|
entrypoint = "src/lib.typ"
|
||||||
|
authors = ["PgBiel <https://github.com/PgBiel>"]
|
||||||
|
categories = ["scripting", "utility"]
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
description = "Framework for custom elements and types in Typst"
|
||||||
|
keywords = ["element", "type", "validation", "styling"]
|
||||||
|
|
||||||
|
[tool.typst-test]
|
||||||
|
tests = "test/unit"
|
||||||
21
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/LICENSE
vendored
Normal file
21
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/LICENSE
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2025 Mc-Zen
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
181
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/README.md
vendored
Normal file
181
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/README.md
vendored
Normal file
@@ -0,0 +1,181 @@
|
|||||||
|
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
[](https://typst.app/universe/package/lilaq)
|
||||||
|
[](https://github.com/lilaq-project/lilaq/actions/workflows/run_tests.yml)
|
||||||
|
[](https://github.com/lilaq-project/lilaq/blob/main/LICENSE)
|
||||||
|
[](https://lilaq.org/)
|
||||||
|
|
||||||
|
_Lilaq is a powerful plotting library for [Typst](https://typst.app/)._
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
You can install the package by cloning the repository and importing Lilaq with
|
||||||
|
```typ
|
||||||
|
#import "@preview/lilaq:0.5.0" as lq
|
||||||
|
```
|
||||||
|
|
||||||
|
The documentation lives at https://lilaq.org.
|
||||||
|
|
||||||
|
## Demos
|
||||||
|
Clicking on any demo image below will bring you to the respective page of the documentation that shows how to create this plot.
|
||||||
|
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Line plot</th>
|
||||||
|
<th>Scatter</th>
|
||||||
|
<th>Bar chart</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/quickstart#the-first-plot)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/scatter)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/bar)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Boxplot</th>
|
||||||
|
<th>Quiver</th>
|
||||||
|
<th>Styled marks</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/boxplot)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/quiver)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/styled-marks)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Errorbars</th>
|
||||||
|
<th>Plot smoothing</th>
|
||||||
|
<th>Stem plot</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/errorbar)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/plot#smooth)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/stem)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
<tr>
|
||||||
|
<th>Colormesh</th>
|
||||||
|
<th>Contour</th>
|
||||||
|
<th>Color bar</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/colormesh)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/contour)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/reference/colorbar)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Spectrum with secondary axis</th>
|
||||||
|
<th>Climograph with twin axis</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/dual-axis)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/climograph)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th>Bar plot with numbers</th>
|
||||||
|
<th>Multiple twin axes</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/bar-plot-with-numbers)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/tutorials/axis#independent-axes-twin-axes)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<th>Plot within a plot</th>
|
||||||
|
<th>Koch snowflake fractal</th>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/plot-within-a-plot)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
|
||||||
|
[](https://lilaq.org/docs/examples/koch-snowflake)
|
||||||
|
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
79
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/bezier-interpolation.typ
vendored
Normal file
79
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/bezier-interpolation.typ
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
#import "linear-system.typ": thomas-algorithm
|
||||||
|
|
||||||
|
/// Solves for a system of linear equations $A dot arrow(x) = arrow(b)$
|
||||||
|
/// for the control points of the Bézier spline in either $x$ or $y$.
|
||||||
|
///
|
||||||
|
/// Returns an array of all control points (including start and end points) in $x$ or $y$.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
#let solve-control-points-1d(
|
||||||
|
/// The matrix $A$ of the system of linear equations.
|
||||||
|
/// -> array
|
||||||
|
A,
|
||||||
|
/// The vector $b$ of the system of linear equations.
|
||||||
|
/// -> array
|
||||||
|
b,
|
||||||
|
/// The dimension $n$ of the system of linear eqautions.
|
||||||
|
/// -> int
|
||||||
|
n,
|
||||||
|
) = {
|
||||||
|
let s = range(2, n).map(i => (
|
||||||
|
2 * (2 * b.at(i - 1) + b.at(i))
|
||||||
|
))
|
||||||
|
s.insert(0, b.at(0) + 2 * b.at(1))
|
||||||
|
s.push(8 * b.at(n - 1) + b.at(n))
|
||||||
|
|
||||||
|
// first control points
|
||||||
|
let c1 = thomas-algorithm(A, s)
|
||||||
|
// second control points
|
||||||
|
let c2 = range(0, n - 1).map(i => 2 * b.at(i + 1) - c1.at(i + 1))
|
||||||
|
c2.push(0.5 * (c1.at(n - 1) + b.at(n)))
|
||||||
|
|
||||||
|
let points = c1.zip(c2, b.slice(1)).flatten()
|
||||||
|
points.insert(0, b.at(0))
|
||||||
|
|
||||||
|
points
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the control points (including start and end points) for a Bézier spline
|
||||||
|
/// which interpolates the given data set.
|
||||||
|
/// For the boundary conditions a curvature of 0 was chosen for the start and end points.
|
||||||
|
///
|
||||||
|
/// Math from https://omaraflak.medium.com/b%C3%A9zier-interpolation-8033e9a262c2
|
||||||
|
///
|
||||||
|
/// Returns an array of points (2D arrays).
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
#let bezier-splines(
|
||||||
|
/// $x$ coordinates of the given points.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
/// $y$ coordinates of the given points.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
) = {
|
||||||
|
let n = y.len() - 1
|
||||||
|
|
||||||
|
if n < 2 {
|
||||||
|
panic("at least 3 points are required for calculating a Bézier spline")
|
||||||
|
}
|
||||||
|
|
||||||
|
let A = ((0,) * (n),) * (n)
|
||||||
|
for i in range(1, n - 1) {
|
||||||
|
A.at(i).at(i) = 4
|
||||||
|
A.at(i + 1).at(i) = 1
|
||||||
|
A.at(i).at(i + 1) = 1
|
||||||
|
}
|
||||||
|
A.at(0).at(0) = 2
|
||||||
|
A.at(1).at(0) = 1
|
||||||
|
A.at(0).at(1) = 1
|
||||||
|
A.at(n - 1).at(n - 1) = 7
|
||||||
|
A.at(n - 1).at(n - 2) = 2
|
||||||
|
|
||||||
|
let points-x = solve-control-points-1d(A, x, n)
|
||||||
|
let points-y = solve-control-points-1d(A, y, n)
|
||||||
|
|
||||||
|
let points = points-x.zip(points-y)
|
||||||
|
|
||||||
|
points
|
||||||
|
}
|
||||||
44
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/boxplot.typ
vendored
Normal file
44
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/boxplot.typ
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
#import "../math.typ": percentile
|
||||||
|
#import "../assertations.typ"
|
||||||
|
|
||||||
|
|
||||||
|
#let boxplot-statistics(x, whiskers: 1.5) = {
|
||||||
|
|
||||||
|
let stat = if type(x) == array {
|
||||||
|
|
||||||
|
import "@preview/komet:0.1.0"
|
||||||
|
return komet.boxplot(x, whisker-pos: whiskers)
|
||||||
|
|
||||||
|
} else if type(x) == dictionary {
|
||||||
|
|
||||||
|
assertations.assert-dict-keys(
|
||||||
|
x,
|
||||||
|
mandatory: ("median", "q1", "q3", "whisker-low", "whisker-high"),
|
||||||
|
optional: ("outliers", "mean"),
|
||||||
|
missing-message: key => "Boxplot data needs to specify \"" + key + "\"",
|
||||||
|
unexpected-message: (key, possible-keys) => "Boxplot data contains unexpected key \"" + key + "\" (expected " + possible-keys + ")"
|
||||||
|
)
|
||||||
|
if not "outliers" in x {
|
||||||
|
x.outliers = ()
|
||||||
|
}
|
||||||
|
x
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert(false, message: "Boxplot data either needs to be an array of values or a dictionary specifying boxplot statistics")
|
||||||
|
}
|
||||||
|
|
||||||
|
let iqr = stat.q3 - stat.q1
|
||||||
|
|
||||||
|
if type(x) == array {
|
||||||
|
let wlo = x.filter(x => x >= stat.q1 - iqr * whiskers)
|
||||||
|
let whi = x.filter(x => x <= stat.q3 + iqr * whiskers)
|
||||||
|
stat.whisker-low = calc.min(stat.q1, wlo.at(0, default: stat.q1))
|
||||||
|
stat.whisker-high = calc.max(stat.q3, whi.at(-1, default: stat.q3))
|
||||||
|
stat.outliers = x.filter(x => x < stat.whisker-low or x > stat.whisker-high)
|
||||||
|
}
|
||||||
|
|
||||||
|
stat.inter-quartile-range = iqr
|
||||||
|
|
||||||
|
|
||||||
|
stat
|
||||||
|
}
|
||||||
818
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/contour.typ
vendored
Normal file
818
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/contour.typ
vendored
Normal file
@@ -0,0 +1,818 @@
|
|||||||
|
#import "../math.typ": vec
|
||||||
|
|
||||||
|
/// Computes the orientation of a polygon, returns `left` if the polygon is
|
||||||
|
/// lefthanded (counter-clockwise) and `right` if it is righthanded (clockwise).
|
||||||
|
#let compute-polygon-orientation(..arr) = {
|
||||||
|
arr = arr.pos()
|
||||||
|
let r = arr.zip(arr.slice(1))
|
||||||
|
.map(((A, B)) => (A.at(0) - B.at(0)) * (A.at(1) + B.at(1)))
|
||||||
|
let s = r.sum(default: 0)
|
||||||
|
if s < -0.0000000000001 { return right }
|
||||||
|
else if s > 0.000000000001 { return left }
|
||||||
|
}
|
||||||
|
// #assert.eq(compute-polygon-orientation( (5, −5),
|
||||||
|
// (−5, −5),
|
||||||
|
// (−1.6666666666666665, −5),
|
||||||
|
// (1.666666666666667, −5),
|
||||||
|
// (5, −5),), none)
|
||||||
|
#assert.eq(compute-polygon-orientation((0,0), (0,1), (1,1), (1,0)), right)
|
||||||
|
#assert.eq(compute-polygon-orientation((0,0), (1,0), (1,1), (0,1)), left)
|
||||||
|
#assert.eq(compute-polygon-orientation((0,0), (1,0)), none)
|
||||||
|
#assert.eq(compute-polygon-orientation((0,0), (0,1)), none)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Retrieves the center coordinate of a hyperbolic paraboloid (HP) surface
|
||||||
|
/// given by the parameter values u=v=0.5.
|
||||||
|
/// p1 ·-----· p2
|
||||||
|
/// | · ←|
|
||||||
|
/// p4 ·-----· p3
|
||||||
|
#let get-hp-center-z(z1, z2, z3, z4) = {
|
||||||
|
return 0.25 * (z1 + z2 + z3 + z4)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Given two points $p_1$, $p_2$ in 3D space, find whether the line between
|
||||||
|
/// theses points intersects a plane $z=z_0$ and at which coordinates $(x,y)$.
|
||||||
|
/// Returns `none` if the segment does not intersect the plane between $p_1$
|
||||||
|
/// and $p_2$.
|
||||||
|
#let intersect-z-plane(p1, p2, z0) = {
|
||||||
|
let (dx, dy, dz) = vec.subtract(p2, p1)
|
||||||
|
if dz == 0 /*and z0 != p1.at(2)*/ { return none }
|
||||||
|
let ratio = (z0 - p1.at(2)) / dz
|
||||||
|
if ratio < 0 or ratio > 1 { return none }
|
||||||
|
return (p1.at(0) + ratio*dx, p1.at(1) + ratio*dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(intersect-z-plane((0,0,0), (2,0,1), .2), (.4, 0))
|
||||||
|
#assert.eq(intersect-z-plane((0,0,0), (2,0,1), 1), (2, 0))
|
||||||
|
#assert.eq(intersect-z-plane((0,0,0), (2,0,1), 1.1), none)
|
||||||
|
#assert.eq(intersect-z-plane((0,0,0), (2,0,1), -.01), none)
|
||||||
|
|
||||||
|
|
||||||
|
/// Given a list of (oriented) segments as pairs of points, returns
|
||||||
|
/// a list of polygons constructed from connected components in the set of
|
||||||
|
/// segments. The orientation plays a role.
|
||||||
|
#let group-segments(segments) = {
|
||||||
|
let cycles = ()
|
||||||
|
while segments.len() > 0 {
|
||||||
|
let segment = segments.pop()
|
||||||
|
let k = segments
|
||||||
|
let (p1, p2) = segment
|
||||||
|
let cycle = (p1, p2)
|
||||||
|
let start = segment
|
||||||
|
while true {
|
||||||
|
let pos = segments.position(x => x.first() == p2)
|
||||||
|
if pos != none {
|
||||||
|
start = segments.remove(pos)
|
||||||
|
p2 = start.last()
|
||||||
|
cycle.push(p2)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cycle = cycle.rev()
|
||||||
|
p1 = cycle.last()
|
||||||
|
while true {
|
||||||
|
let pos = segments.position(x => x.last() == p1)
|
||||||
|
if pos != none {
|
||||||
|
p1 = segments.remove(pos).first()
|
||||||
|
cycle.push(p1)
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cycles.push(cycle)
|
||||||
|
}
|
||||||
|
return cycles
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let close-path-at-boundaries(path, boundaries: (xmin: 0, xmax: 1, ymin: 0, ymax: 1)) = {
|
||||||
|
let corners = (
|
||||||
|
(boundaries.xmax, boundaries.ymax),
|
||||||
|
(boundaries.xmax, boundaries.ymin),
|
||||||
|
(boundaries.xmin, boundaries.ymin),
|
||||||
|
(boundaries.xmin, boundaries.ymax)
|
||||||
|
)
|
||||||
|
if path.first() == path.last() { return path.rev() }
|
||||||
|
let get-case(point) = {
|
||||||
|
let case = 0
|
||||||
|
|
||||||
|
}
|
||||||
|
let get-edge(point) = {
|
||||||
|
if point.at(1) == boundaries.ymax { return 0 } // top
|
||||||
|
if point.at(1) == boundaries.ymin { return 2 } // bottom
|
||||||
|
if point.at(0) == boundaries.xmax { return 1 } // right
|
||||||
|
if point.at(0) == boundaries.xmin { return 3 } // left
|
||||||
|
let k = point
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
let edge1 = get-edge(path.last())
|
||||||
|
let edge2 = get-edge(path.first())
|
||||||
|
if none in (edge1, edge2) { return none }
|
||||||
|
|
||||||
|
|
||||||
|
let edges = (edge1, edge2)
|
||||||
|
|
||||||
|
let rrange(a, b) = {
|
||||||
|
if a < b { a+= 4}
|
||||||
|
let r = calc.rem(1 - 3, 4)
|
||||||
|
|
||||||
|
if a <= b { range(a, b) }
|
||||||
|
else {
|
||||||
|
return range(b, a).rev()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let f = rrange(edge1, edge2)
|
||||||
|
if calc.rem-euclid(edge1 - edge2, 4) > 2 {
|
||||||
|
f = rrange(edge2, edge1).rev()
|
||||||
|
} else {
|
||||||
|
f = rrange(edge1, edge2)
|
||||||
|
}
|
||||||
|
for i in f {
|
||||||
|
path.push(corners.at(calc.rem(i, 4)))
|
||||||
|
}
|
||||||
|
path.push(path.first())
|
||||||
|
return path.rev()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let mod-distance(a, b, div) = {
|
||||||
|
let diff = b - a
|
||||||
|
calc.abs(calc.rem(diff, div))
|
||||||
|
}
|
||||||
|
#assert.eq(mod-distance(0, 1, 4), 1)
|
||||||
|
#assert.eq(mod-distance(1, 0, 4), 1)
|
||||||
|
#assert.eq(mod-distance(1, 2, 4), 1)
|
||||||
|
#assert.eq(mod-distance(0, 2, 4), 2)
|
||||||
|
// #assert.eq(mod-distance(3, 0, 4), 1)
|
||||||
|
#assert.eq(mod-distance(2, 2, 4), 0)
|
||||||
|
#assert.eq(mod-distance(0, 2, 4), 2)
|
||||||
|
#assert.eq(mod-distance(2, 0, 4), 2)
|
||||||
|
|
||||||
|
#assert.eq(mod-distance(0, 5, 4), 1)
|
||||||
|
|
||||||
|
#let close-path-at-boundaries(path, boundaries: (xmin: 0, xmax: 1, ymin: 0, ymax: 1)) = {
|
||||||
|
let corners = (
|
||||||
|
(boundaries.xmax, boundaries.ymax),
|
||||||
|
(boundaries.xmax, boundaries.ymin),
|
||||||
|
(boundaries.xmin, boundaries.ymin),
|
||||||
|
(boundaries.xmin, boundaries.ymax)
|
||||||
|
)
|
||||||
|
if path.len() == 0 or path.first() == path.last() { return path.rev() }
|
||||||
|
let get-edge(point) = {
|
||||||
|
if point.at(1) == boundaries.ymax { return 0 } // top
|
||||||
|
if point.at(1) == boundaries.ymin { return 2 } // bottom
|
||||||
|
if point.at(0) == boundaries.xmax { return 1 } // right
|
||||||
|
if point.at(0) == boundaries.xmin { return 3 } // left
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
|
||||||
|
let edge1 = get-edge(path.last())
|
||||||
|
let edge2 = get-edge(path.first())
|
||||||
|
if none in (edge1, edge2) { return none }
|
||||||
|
|
||||||
|
|
||||||
|
if edge1 == edge2 {
|
||||||
|
path.push(path.first())
|
||||||
|
return path.rev()
|
||||||
|
} else if calc.rem(edge1 + 2, 4) == edge2 { // opposite edges
|
||||||
|
if edge1 in (0,2) { // top/bottom
|
||||||
|
let mid-x = 0.5 * (boundaries.xmax - boundaries.xmin)
|
||||||
|
// if path.last().at(0) < mid-x
|
||||||
|
let vertices = (corners.at(0), corners.at(1))
|
||||||
|
if edge1 == 2 { vertices = vertices.rev() }
|
||||||
|
path += vertices
|
||||||
|
} else { // left/right
|
||||||
|
let vertices = (corners.at(3), corners.at(0))
|
||||||
|
// let vertices = (corners.at(2), corners.at(1))
|
||||||
|
if edge1 == 1 { vertices = vertices.rev() }
|
||||||
|
path += vertices
|
||||||
|
if path.last().last() < -3 {
|
||||||
|
let k = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if calc.rem(edge1 + 1, 4) == edge2 {
|
||||||
|
path.push(corners.at(edge1))
|
||||||
|
} else if calc.rem(edge2 + 1, 4) == edge1 {
|
||||||
|
path.push(corners.at(edge2))
|
||||||
|
} else { assert(false) }
|
||||||
|
}
|
||||||
|
path.push(path.first())
|
||||||
|
return path.rev()
|
||||||
|
|
||||||
|
let edges = (edge1, edge2)
|
||||||
|
|
||||||
|
let rrange(a, b) = {
|
||||||
|
if a < b { a+= 4}
|
||||||
|
let r = calc.rem(1 - 3, 4)
|
||||||
|
|
||||||
|
if a <= b { range(a, b) }
|
||||||
|
else {
|
||||||
|
return range(b, a).rev()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let f = rrange(edge1, edge2)
|
||||||
|
if calc.rem-euclid(edge1 - edge2, 4) > 2 {
|
||||||
|
f = rrange(edge2, edge1).rev()
|
||||||
|
} else {
|
||||||
|
f = rrange(edge1, edge2)
|
||||||
|
}
|
||||||
|
for i in f {
|
||||||
|
path.push(corners.at(calc.rem(i, 4)))
|
||||||
|
}
|
||||||
|
path.push(path.first())
|
||||||
|
return path.rev()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let test-close-boundaries(curve, result) = {
|
||||||
|
let test-boundaries = (xmin: 0, xmax: 1, ymin: 0, ymax: 1)
|
||||||
|
let closed-curve = close-path-at-boundaries(curve, boundaries: test-boundaries)
|
||||||
|
if result != none {
|
||||||
|
assert.eq(closed-curve, result.rev())
|
||||||
|
}
|
||||||
|
box(width: 1cm, height: 1cm, fill: luma(90%), {
|
||||||
|
let transform(p) = (p.at(0)*1cm, 1cm - p.at(1)*1cm)
|
||||||
|
let vertices = closed-curve.map(transform)
|
||||||
|
place(std.curve(
|
||||||
|
std.curve.move(vertices.first()),
|
||||||
|
..vertices.slice(1).map(std.curve.line),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// #test-close-boundaries((), ())
|
||||||
|
#test-close-boundaries(((.5,0), (1,.5)),
|
||||||
|
((.5, 0), (1, .5), (1, 0), (.5, 0))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((1,.5), (.5,0)),
|
||||||
|
((1, .5), (.5, 0), (1, 0), (1, .5))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((1,.5), (.5,1)),
|
||||||
|
((1, .5), (.5, 1), (1, 1), (1, .5))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((.5,1), (1,.5)),
|
||||||
|
((.5, 1), (1, .5), (1, 1), (.5, 1))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0,.5), (.5,1)),
|
||||||
|
((0, .5), (.5, 1), (0, 1), (0, .5))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((.5,1), (0,.5)),
|
||||||
|
((.5, 1), (0, .5), (0, 1), (.5, 1))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0,.5), (.5,0)),
|
||||||
|
((0, .5), (.5, 0), (0, 0), (0, .5))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((.5,0), (0,.5)),
|
||||||
|
((.5, 0), (0, .5), (0, 0), (.5, 0))
|
||||||
|
)
|
||||||
|
|
||||||
|
#test-close-boundaries(((.2,0), (.5, .5), (.8, 0)),
|
||||||
|
((.2,0), (.5, .5), (.8, 0), (.2, 0))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((.2, 1), (.5, .5), (.8, 1)),
|
||||||
|
((.2, 1), (.5, .5), (.8, 1), (.2, 1))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0, .2), (.5, .5), (0, .8)),
|
||||||
|
((0, .2), (.5, .5), (0, .8), (0, .2))
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((1, .2), (.5, .5), (1, .8)),
|
||||||
|
((1, .2), (.5, .5), (1, .8), (1, .2))
|
||||||
|
)
|
||||||
|
|
||||||
|
#test-close-boundaries(((1, 0), (0, 1)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0, 1), (1, 0)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((1, 1), (0, 0)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0, 0), (1, 1)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#test-close-boundaries(((1, 0), (0, .5)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((1, 0), (.5, 1)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0, .2), (1, .3)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
#test-close-boundaries(((0, .7), (1, .8)),
|
||||||
|
none
|
||||||
|
)
|
||||||
|
|
||||||
|
#test-close-boundaries(((1, 0), (1, 1)), none)
|
||||||
|
#test-close-boundaries(((1, 1), (1, 0)), none)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let generate-contour(x, y, z, level, z-range: 1) = {
|
||||||
|
let get-z(i, j) = { z.at(i).at(j) }
|
||||||
|
let get-p(i, j) = { (x.at(i), y.at(j), z.at(i).at(j)) }
|
||||||
|
|
||||||
|
let blocks = ()
|
||||||
|
for i in range(x.len() - 1) {
|
||||||
|
for j in range(y.len() - 1) {
|
||||||
|
let zs = (get-z(i, j), get-z(i+1, j), get-z(i, j+1), get-z(i+1,j+1))
|
||||||
|
if calc.min(..zs) <= level and calc.max(..zs) >= level {
|
||||||
|
blocks.push((i, j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments = ()
|
||||||
|
while blocks.len() > 0 {
|
||||||
|
let (i, j) = blocks.pop()
|
||||||
|
/*
|
||||||
|
0: → (0,0) -> (1,0)
|
||||||
|
1: ↓ (1,0) -> (1,1)
|
||||||
|
2: ← (1,1) -> (0,1)
|
||||||
|
3: ↑ (0,1) -> (0,0)
|
||||||
|
*/
|
||||||
|
let qqqs = ((0,0), (1,0), (1,1), (0,1), (0,0))
|
||||||
|
let intersections = ()
|
||||||
|
for edge in range(4) {
|
||||||
|
let (Ax, Ay) = qqqs.at(edge)
|
||||||
|
let (Bx, By) = qqqs.at(edge + 1)
|
||||||
|
let p1 = get-p(Ax + i, Ay + j)
|
||||||
|
let p2 = get-p(Bx + i, By + j)
|
||||||
|
if edge < 2 { (p1, p2) = (p2, p1) }
|
||||||
|
let interpolation = intersect-z-plane(p1, p2, level)
|
||||||
|
if interpolation == p2.slice(0,2) and edge > 2 {
|
||||||
|
// edge += 1
|
||||||
|
}
|
||||||
|
if interpolation != none {
|
||||||
|
intersections.push((edge, interpolation))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
intersections = intersections.dedup(key: x => x.at(1))
|
||||||
|
let sort-intersection-tuple(i1, i2) = {
|
||||||
|
let corner = qqqs.at(i2.at(0))
|
||||||
|
if corner == i2.at(1) {
|
||||||
|
// (i1, i2) = (i2, i1)
|
||||||
|
}
|
||||||
|
if get-z(..vec-add(corner, (i,j))) < level {
|
||||||
|
(i1, i2) = (i2, i1)
|
||||||
|
}
|
||||||
|
return (i1.at(1), i2.at(1))
|
||||||
|
}
|
||||||
|
|
||||||
|
// assert(intersections.len() in (2,4))
|
||||||
|
if intersections.len() == 2 {
|
||||||
|
segments.push(sort-intersection-tuple(..intersections))
|
||||||
|
} else if intersections.len() == 4 {
|
||||||
|
let z0 = get-hp-center-z(get-z(i, j), get-z(i+1, j), get-z(i, j+1), get-z(i+1,j+1))
|
||||||
|
let (a,b,c,d) = intersections
|
||||||
|
if calc.abs(z0 - level) < 1e-6* z-range {
|
||||||
|
let center = (0.5*(x.at(i) + x.at(i+1)), 0.5*(y.at(j) + y.at(j+1)))
|
||||||
|
let (c1, c2) = (center, center)
|
||||||
|
c1.at(0) += 1e-30
|
||||||
|
c2.at(0) -= 1e-30
|
||||||
|
c1.at(0) += .2
|
||||||
|
c2.at(0) -= .2
|
||||||
|
if get-z(i, j) > level{ (c1, c2) = (c2, c1)}
|
||||||
|
let seg1 = sort-intersection-tuple(a, d)
|
||||||
|
let seg2 = sort-intersection-tuple(b, c)
|
||||||
|
segments.push((seg1.at(0), c1))
|
||||||
|
segments.push((seg2.at(0), c2))
|
||||||
|
|
||||||
|
if get-z(i, j) > level {
|
||||||
|
(c1, c2) = (c2, c1)
|
||||||
|
}
|
||||||
|
segments.push((c2, seg1.at(1)))
|
||||||
|
segments.push((c1, seg2.at(1)))
|
||||||
|
// segments.push((a,b,c,d))
|
||||||
|
} else if z0 > level {
|
||||||
|
if get-z(i, j) > level {
|
||||||
|
(b, d) = (d, b)
|
||||||
|
}
|
||||||
|
segments.push(sort-intersection-tuple(a,d))
|
||||||
|
segments.push(sort-intersection-tuple(c,b))
|
||||||
|
// segments.push((a,d,b,c))
|
||||||
|
} else if level > z0 {
|
||||||
|
if get-z(i, j) > level {
|
||||||
|
(d, b) = (b, d)
|
||||||
|
}
|
||||||
|
segments.push(sort-intersection-tuple(a,b))
|
||||||
|
segments.push(sort-intersection-tuple(c,d))
|
||||||
|
// segments.push((a,c,b,d))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// segments.push(sort-intersection-tuple(..intersections.slice(0,2)))
|
||||||
|
// let o = (i,j)
|
||||||
|
// assert(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let paths = group-segments(segments)
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
#let lshift(a) = 2*a
|
||||||
|
|
||||||
|
#let bw-or(a,b) = {
|
||||||
|
let c = 0;
|
||||||
|
let n = 1;
|
||||||
|
while ((a > 0) or (b > 0)) {
|
||||||
|
if ((calc.rem(a, 2) == 1) or (calc.rem(b, 2) == 1)) {
|
||||||
|
c += n;
|
||||||
|
}
|
||||||
|
a = calc.quo(a, 2);
|
||||||
|
b = calc.quo(b, 2);
|
||||||
|
n = n * 2;
|
||||||
|
}
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(bw-or(0b00, 0b11), 0b11)
|
||||||
|
#assert.eq(bw-or(0b010001, 0b110100), 0b110101)
|
||||||
|
|
||||||
|
#let compute-case(corners-z, level) = {
|
||||||
|
let case = 0
|
||||||
|
for z in corners-z {
|
||||||
|
case = bw-or(lshift(case), if z > level {1} else {0})
|
||||||
|
}
|
||||||
|
if case == 0 {
|
||||||
|
if corners-z.filter(x => x == level).len() == 2 {
|
||||||
|
for z in corners-z {
|
||||||
|
case = bw-or(lshift(case), if z >= level {1} else {0})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return case
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(compute-case((-1,-1,-1,-1), 0), 0)
|
||||||
|
#assert.eq(compute-case((1,1,1,1), 0), 15)
|
||||||
|
#assert.eq(compute-case((-1,-1,-1,1), 0), 1)
|
||||||
|
#assert.eq(compute-case((-1,-1,1,-1), 0), 2)
|
||||||
|
#assert.eq(compute-case((-1,-1,1,1), 0), 3)
|
||||||
|
#assert.eq(compute-case((-1,1,-1,-1), 0), 4)
|
||||||
|
|
||||||
|
#assert.eq(compute-case((-1,-1,-1,1), 0), 1)
|
||||||
|
#assert.eq(compute-case((0,0,-1,-1), 0), 12)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let generate-contour(x, y, z, level, z-range: 1) = {
|
||||||
|
let z-eps = 1e-6 * z-range
|
||||||
|
let x-eps = 1e-17 * (x.last() - x.first())
|
||||||
|
|
||||||
|
let get-z(i, j) = { z.at(j).at(i) }
|
||||||
|
let get-p(i, j) = { (x.at(i), y.at(j), z.at(j).at(i)) }
|
||||||
|
|
||||||
|
let blocks = ()
|
||||||
|
for i in range(x.len() - 1) {
|
||||||
|
for j in range(y.len() - 1) {
|
||||||
|
let zs = (get-z(i, j), get-z(i+1, j), get-z(i, j+1), get-z(i+1,j+1))
|
||||||
|
if calc.min(..zs) <= level and calc.max(..zs) >= level {
|
||||||
|
blocks.push((i, j))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let segments = ()
|
||||||
|
while blocks.len() > 0 {
|
||||||
|
let (i, j) = blocks.pop()
|
||||||
|
let corners = ((0,0), (1,0), (1,1), (0,1))
|
||||||
|
let case = compute-case(corners.map(corner => get-p(..vec.add((i, j), corner)).at(2)), level)
|
||||||
|
let qqqs = ((0,0), (1,0), (1,1), (0,1), (0,0))
|
||||||
|
|
||||||
|
let get-edge-intersection(edge) = {
|
||||||
|
let (Ax, Ay) = qqqs.at(edge)
|
||||||
|
let (Bx, By) = qqqs.at(edge + 1)
|
||||||
|
let p1 = get-p(Ax + i, Ay + j)
|
||||||
|
let p2 = get-p(Bx + i, By + j)
|
||||||
|
if edge < 2 { (p1, p2) = (p2, p1) }
|
||||||
|
|
||||||
|
let interpolation = intersect-z-plane(p1, p2, level)
|
||||||
|
interpolation
|
||||||
|
}
|
||||||
|
let get-line-segment(edge1, edge2) = ((edge2, edge1).map(get-edge-intersection),)
|
||||||
|
|
||||||
|
let z0 = get-hp-center-z(get-z(i, j), get-z(i+1, j), get-z(i, j+1), get-z(i+1,j+1))
|
||||||
|
|
||||||
|
let cases = (
|
||||||
|
() => (),
|
||||||
|
() => get-line-segment(3,2),
|
||||||
|
() => get-line-segment(2,1),
|
||||||
|
() => get-line-segment(3,1),
|
||||||
|
() => get-line-segment(1,0),
|
||||||
|
() => {
|
||||||
|
if calc.abs(z0 - level) < z-eps {
|
||||||
|
let (i0,i1,i2,i3) = (0,1,2,3).map(get-edge-intersection)
|
||||||
|
let center = (i0.at(0), i1.at(1))
|
||||||
|
let (c1, c2) = (center, center)
|
||||||
|
c1.at(0) += x-eps
|
||||||
|
c2.at(0) -= x-eps
|
||||||
|
|
||||||
|
((i0, c2), (c2, i3), (i2, c1), (c1, i1))
|
||||||
|
} else if z0 > level {
|
||||||
|
get-line-segment(3,0) + get-line-segment(1,2)
|
||||||
|
} else if z0 < level {
|
||||||
|
get-line-segment(1,0) + get-line-segment(3,2)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => get-line-segment(2,0),
|
||||||
|
() => get-line-segment(3,0),
|
||||||
|
() => get-line-segment(0,3),
|
||||||
|
() => get-line-segment(0,2),
|
||||||
|
() => {
|
||||||
|
if calc.abs(z0 - level) < z-eps {
|
||||||
|
let (i0,i1,i2,i3) = (0,1,2,3).map(get-edge-intersection)
|
||||||
|
let center = (i0.at(0), i1.at(1))
|
||||||
|
let (c1, c2) = (center, center)
|
||||||
|
c1.at(0) -= x-eps
|
||||||
|
c2.at(0) += x-eps
|
||||||
|
|
||||||
|
((i1, c2), (c2, i0), (i3, c1), (c1, i2))
|
||||||
|
} else if z0 > level {
|
||||||
|
get-line-segment(0,1) + get-line-segment(2,3)
|
||||||
|
} else if z0 < level {
|
||||||
|
get-line-segment(0,3) + get-line-segment(2,1)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
() => get-line-segment(0,1),
|
||||||
|
() => get-line-segment(1,3),
|
||||||
|
() => get-line-segment(1,2),
|
||||||
|
() => get-line-segment(2,3),
|
||||||
|
() => (),
|
||||||
|
)
|
||||||
|
let k = (i,j,case, (get-z(i, j), get-z(i+1, j), get-z(i, j+1), get-z(i+1,j+1)))
|
||||||
|
segments += cases.at(case)()
|
||||||
|
}
|
||||||
|
let paths = group-segments(segments.dedup())
|
||||||
|
return paths
|
||||||
|
}
|
||||||
|
|
||||||
|
#let generate-contours(x, y, z, levels, z-range: 1) = {
|
||||||
|
return levels.map(level => generate-contour(x, y, z, level, z-range: z-range))
|
||||||
|
}
|
||||||
|
|
||||||
|
#let gen-contour-1rect = generate-contour.with((0,1), (0,1))
|
||||||
|
|
||||||
|
|
||||||
|
#let test-contour-case(x: (0,1), y: (0,1), z, level, answer) = {
|
||||||
|
let z-flat = z.flatten()
|
||||||
|
let (min, max) = (calc.min(..z-flat), calc.max(..z-flat))
|
||||||
|
if min == max {min -=1 }
|
||||||
|
let contour = generate-contour(x, y, z, level)
|
||||||
|
assert.eq(contour, answer)
|
||||||
|
box(width: 2cm, height: 2cm, stroke: .5pt,
|
||||||
|
{
|
||||||
|
let sq = box.with(width: 1cm, height: 1cm)
|
||||||
|
let cmap = color.map.icefire
|
||||||
|
let cmap = (blue, yellow, red)
|
||||||
|
let grad = gradient.linear(..cmap)
|
||||||
|
let get-clr(z) = grad.sample((z - min)/(max - min)*100%)
|
||||||
|
for i in range(x.len()) {
|
||||||
|
for j in range(y.len()) {
|
||||||
|
place(
|
||||||
|
dx: i * 1cm, dy: 1cm - j*1cm,
|
||||||
|
sq(fill: get-clr(z.at(j).at(i)))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let transform(p) = (p.at(0)*2cm, 2cm - p.at(1)*2cm)
|
||||||
|
contour.map(contour-path => {
|
||||||
|
contour-path = contour-path.map(transform)
|
||||||
|
place(curve(
|
||||||
|
curve.move(contour-path.first()),
|
||||||
|
..contour-path.slice(1).map(curve.line),
|
||||||
|
stroke: get-clr(level).lighten(20%) + 2pt,
|
||||||
|
)) + place(
|
||||||
|
dx: (contour-path.first()).at(0),
|
||||||
|
dy: (contour-path.first()).at(1),
|
||||||
|
place(center + horizon, circle(fill: black, radius: 1pt))
|
||||||
|
)}).join()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#test-contour-case(
|
||||||
|
x: (-1,0,1),
|
||||||
|
y: (-5,5),
|
||||||
|
// ((-1,-1),(0,0),(1,1)),
|
||||||
|
((-1,0,1),(-1,0,1)),
|
||||||
|
0,
|
||||||
|
((((0, 5), (0, -5)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
#set page(height: auto, width: auto, margin: 1cm)
|
||||||
|
|
||||||
|
The dot indicates the start of a curve. A curve is to be oriented such that the _higher_ profile lies _left_ of it.
|
||||||
|
|
||||||
|
|
||||||
|
== No intersection
|
||||||
|
|
||||||
|
#set table(align: center)
|
||||||
|
|
||||||
|
#table(columns: 3,
|
||||||
|
[Right on], [all below], [all above],
|
||||||
|
test-contour-case(((0,0),(0,0)), 0, ()),
|
||||||
|
test-contour-case(((20,-20),(-20,10)), 50, ()),
|
||||||
|
test-contour-case(((20,-20),(-20,10)), -50, ()),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
== Saddles
|
||||||
|
|
||||||
|
// HP saddle directly through center
|
||||||
|
#table(columns: 6,
|
||||||
|
[Right on], [slightly above], [slightly below],
|
||||||
|
[Right on], [slightly above], [slightly below],
|
||||||
|
test-contour-case(((-1,1),(1,-1)), 0,
|
||||||
|
(((1, .5), (.5,.5), (.5,0)), ((0,.5), (.5,.5), (.5, 1)))
|
||||||
|
),
|
||||||
|
test-contour-case(((-1,1),(1,-1)), 0.5,
|
||||||
|
((((0,.75), (.25, 1)), ((1,.25), (.75,0))))
|
||||||
|
),
|
||||||
|
test-contour-case(((-1,1),(1,-1)), -0.5,
|
||||||
|
((((1,.75), (.75,1)), ((0,.25), (.25, 0))))
|
||||||
|
),
|
||||||
|
test-contour-case(((1,-1),(-1,1)), 0,
|
||||||
|
(((.5,1), (.5,.5), (1, .5)), ((.5, 0),(.5,.5), (0,.5)))
|
||||||
|
),
|
||||||
|
test-contour-case(((1,-1),(-1,1)), 0.5,
|
||||||
|
((((.75,1), (1,.75)), ((.25,0), (0,.25))))
|
||||||
|
),
|
||||||
|
test-contour-case(((1,-1),(-1,1)), -0.5,
|
||||||
|
((((.25,1), (0,.75)), ((.75,0), (1,.25))))
|
||||||
|
),
|
||||||
|
[Offset y], [Offset x], [skew below], [], [], [],
|
||||||
|
|
||||||
|
test-contour-case(
|
||||||
|
((-3,3),(1,-1)),
|
||||||
|
0,
|
||||||
|
(((1, .75), (.5,.75), (.5,0)),
|
||||||
|
((0,.75), (.5,.75), (.5, 1)))
|
||||||
|
),
|
||||||
|
test-contour-case(
|
||||||
|
((-3,1),(3,-1)), 0,
|
||||||
|
(((1, .5), (.75,.5), (.75,0)), ((0,.5), (.75,.5), (.75, 1)))
|
||||||
|
),
|
||||||
|
test-contour-case(((-3,3),(3,-1)), 0,
|
||||||
|
(((1, .75), (.75,1)), ((0,.5), (.5, 0)))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
== Cross to opposite side
|
||||||
|
|
||||||
|
#table(columns: 4,
|
||||||
|
[Left to right], [Right to left], [Top to bottom], [Bottom to top],
|
||||||
|
// o o
|
||||||
|
// ---→
|
||||||
|
// x x
|
||||||
|
test-contour-case(((1,0),(2, 6)), 1.5,
|
||||||
|
((((0,.5), (1, .25)), ))
|
||||||
|
),
|
||||||
|
// x x
|
||||||
|
// ←---
|
||||||
|
// o o
|
||||||
|
test-contour-case(((2,6),(1, 0)), 1.5,
|
||||||
|
((((1,.75), (0, .5)), ))
|
||||||
|
),
|
||||||
|
// x | o
|
||||||
|
// |
|
||||||
|
// x ↓ o
|
||||||
|
test-contour-case(((1,2),(0, 6)), 1.5,
|
||||||
|
((((.25,1), (.5, 0)), ))
|
||||||
|
),
|
||||||
|
// o ↑ x
|
||||||
|
// |
|
||||||
|
// o | x
|
||||||
|
test-contour-case(((2,0),(6,1)), 1.5,
|
||||||
|
((((.25,0), (.9,1)), ))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
// let mesh_ = create-mesh(linspace(-5, 5, num: 3), linspace(-5, 5, num: 2), func)
|
||||||
|
|
||||||
|
== Corners
|
||||||
|
|
||||||
|
#table(columns: 4,
|
||||||
|
[Bottom to left], [Left to bottom],[Right to bottom],[Bottom to right],
|
||||||
|
// x x
|
||||||
|
// ←-|
|
||||||
|
// o | x (o above level)
|
||||||
|
test-contour-case(((6,2),(0,0)), 3,
|
||||||
|
((((.75,0), (0,.5)), ))
|
||||||
|
),
|
||||||
|
// o o
|
||||||
|
// --|
|
||||||
|
// x ↓ o (x below level)
|
||||||
|
test-contour-case(((-4,2),(0,0)), -1,
|
||||||
|
((((0,.75), (.5,0)), ))
|
||||||
|
),
|
||||||
|
// x x
|
||||||
|
// |--
|
||||||
|
// x ↓ o
|
||||||
|
test-contour-case(((-4,4),(-1,-1)), 0,
|
||||||
|
((((1,.8), (.5,0)), ))
|
||||||
|
),
|
||||||
|
// o o
|
||||||
|
// |-→
|
||||||
|
// o | x
|
||||||
|
test-contour-case(((4,-4),(1,1)), 0,
|
||||||
|
((((.5,0), (1,.8)), ))
|
||||||
|
),
|
||||||
|
[Top to right], [Right to top],[Left to top],[Top to left],
|
||||||
|
|
||||||
|
// x | o
|
||||||
|
// |-→
|
||||||
|
// x x
|
||||||
|
test-contour-case(((-4,-4),(-4,4)), 0,
|
||||||
|
((((.5,1), (1,.5)), ))
|
||||||
|
),
|
||||||
|
// o ↑ x
|
||||||
|
// |--
|
||||||
|
// o o
|
||||||
|
test-contour-case(((4,4),(4,-4)), 0,
|
||||||
|
((((1,.5), (.5,1)), ))
|
||||||
|
),
|
||||||
|
// o ↑ x
|
||||||
|
// --|
|
||||||
|
// x x
|
||||||
|
test-contour-case(((-4,-4),(4,-4)), 0,
|
||||||
|
((((0,.5), (.5,1)), ))
|
||||||
|
),
|
||||||
|
// x | o
|
||||||
|
// ←-|
|
||||||
|
// o o
|
||||||
|
test-contour-case(((4,4),(-4,4)), 0,
|
||||||
|
((((.5,1), (0,.5)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
== Edge cases
|
||||||
|
|
||||||
|
#table(columns: 4,
|
||||||
|
[Top below], [Top above], [Bottom below], [Bottom above],
|
||||||
|
// ·←--·
|
||||||
|
//
|
||||||
|
// o o
|
||||||
|
test-contour-case(((3,4),(0,0)), 0,
|
||||||
|
((((1,1), (0,1)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// ·--→·
|
||||||
|
//
|
||||||
|
// x x
|
||||||
|
test-contour-case(((-3,-4),(0,0)), 0,
|
||||||
|
((((0,1), (1,1)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// o o
|
||||||
|
//
|
||||||
|
// ·--→·
|
||||||
|
test-contour-case(((0,0),(3,4)), 0,
|
||||||
|
((((0,0), (1,0)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// x x
|
||||||
|
//
|
||||||
|
// ·←--·
|
||||||
|
test-contour-case(((0,0),(-3,-4)), 0,
|
||||||
|
((((1,0), (0,0)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
[Left below], [Left above], [Top below], [Top above],
|
||||||
|
// · o
|
||||||
|
// ↓
|
||||||
|
// · o
|
||||||
|
test-contour-case(((0,3),(0,4)), 0,
|
||||||
|
((((0,1), (0,0)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// · x
|
||||||
|
// ↑
|
||||||
|
// · x
|
||||||
|
test-contour-case(((0,-3),(0,-4)), 0,
|
||||||
|
((((0,0), (0,1)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// o ·
|
||||||
|
// ↑
|
||||||
|
// o ·
|
||||||
|
test-contour-case(((3,0),(4,0)), 0,
|
||||||
|
((((1,0), (1,1)), ))
|
||||||
|
),
|
||||||
|
|
||||||
|
// x ·
|
||||||
|
// ↓
|
||||||
|
// x ·
|
||||||
|
test-contour-case(((-3,0),(-4,0)), 0,
|
||||||
|
((((1,1), (1,0)), ))
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
9
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/gaussian-kde.typ
vendored
Normal file
9
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/gaussian-kde.typ
vendored
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
|
||||||
|
|
||||||
|
#let gaussian-kde(x, bw-method) = {
|
||||||
|
if type(bw-method) in (int, float) {
|
||||||
|
bw-method = (x => bw-method)
|
||||||
|
}
|
||||||
|
let factor = bw-method()
|
||||||
|
let dim = x.len()
|
||||||
|
}
|
||||||
36
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/interpolate.typ
vendored
Normal file
36
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/interpolate.typ
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/// Computes the value at a rational index into an array by performing linear
|
||||||
|
/// interpolation. If the index is an integer, the exact value at the index is
|
||||||
|
/// returned.
|
||||||
|
///
|
||||||
|
/// -> int | float
|
||||||
|
#let linear(
|
||||||
|
|
||||||
|
/// Data values to interpolate from.
|
||||||
|
/// -> array
|
||||||
|
values,
|
||||||
|
|
||||||
|
/// Rational index into the array.
|
||||||
|
/// -> int | float
|
||||||
|
index
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let n = values.len()
|
||||||
|
assert(n > 0, message: "An array with at least one element is required for interpolation")
|
||||||
|
assert(0 <= index and index <= n - 1, message: "Index " + str(index) + " out of range for interpolation with array of length " + str(n))
|
||||||
|
if index < 0 { return values.first() }
|
||||||
|
if index >= n - 1 { return values.last() }
|
||||||
|
let lower = calc.floor(index)
|
||||||
|
let upper = lower + 1
|
||||||
|
let t = calc.fract(index)
|
||||||
|
values.at(lower) * (1 - t) + values.at(upper) * t
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(linear((1,2,8), 0), 1)
|
||||||
|
#assert.eq(linear((1,2,8), 0.5), 1.5)
|
||||||
|
#assert.eq(linear((1,2,8), 1.5), 5)
|
||||||
|
#assert.eq(linear((1,2,8), 1), 2)
|
||||||
|
#assert.eq(linear((1,2,8), 2), 8)
|
||||||
59
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/linear-system.typ
vendored
Normal file
59
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/algorithm/linear-system.typ
vendored
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
|
||||||
|
/// Solve a system of linear equations $A dot arrow(x) = arrow(b)$
|
||||||
|
/// where $A$ is a tridiagonal matrix.
|
||||||
|
/// See https://en.wikipedia.org/wiki/Tridiagonal_matrix_algorithm
|
||||||
|
/// for more information.
|
||||||
|
///
|
||||||
|
/// Returns the solutions $arrow(x) in RR^n$ of the system of linear equqations.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
#let thomas-algorithm(
|
||||||
|
/// The matrix $A in RR^(n times n)$ of the system of linear equations.
|
||||||
|
/// The data format is an array of arrays, in row-major order.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
A,
|
||||||
|
/// The vector $arrow(b) in RR^n$ of the system of linear equations.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
b,
|
||||||
|
) = {
|
||||||
|
let n = b.len()
|
||||||
|
|
||||||
|
if n == 1 {
|
||||||
|
return (b.at(0) / A.at(0).at(0),)
|
||||||
|
}
|
||||||
|
|
||||||
|
let beta = (0,) * n
|
||||||
|
let gamma = (0,) * n
|
||||||
|
let y = (0,) * n
|
||||||
|
|
||||||
|
beta.at(0) = A.at(0).at(0)
|
||||||
|
gamma.at(0) = A.at(0).at(1) / beta.at(0)
|
||||||
|
y.at(0) = b.at(0) / beta.at(0)
|
||||||
|
|
||||||
|
for i in range(1, n) {
|
||||||
|
let d-i = A.at(i).at(i)
|
||||||
|
let e-i = A.at(i).at(i - 1)
|
||||||
|
beta.at(i) = d-i - e-i * gamma.at(i - 1)
|
||||||
|
|
||||||
|
if i < n - 1 {
|
||||||
|
let c-i = A.at(i).at(i + 1)
|
||||||
|
gamma.at(i) = c-i / beta.at(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// backward elimination
|
||||||
|
y.at(i) = (b.at(i) - e-i * y.at(i - 1)) / beta.at(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
let x = (0,) * n
|
||||||
|
x.at(n - 1) = y.at(n - 1)
|
||||||
|
|
||||||
|
// forward elimination
|
||||||
|
for i in range(n - 2, -1, step: -1) {
|
||||||
|
x.at(i) = y.at(i) - gamma.at(i) * x.at(i + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
x
|
||||||
|
}
|
||||||
|
|
||||||
154
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/assertations.typ
vendored
Normal file
154
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/assertations.typ
vendored
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
|
||||||
|
/// Assert that two floats or arrays are equal up to a (configurable) epsilon.
|
||||||
|
#let approx(
|
||||||
|
|
||||||
|
/// First value.
|
||||||
|
a,
|
||||||
|
|
||||||
|
/// Second value.
|
||||||
|
b,
|
||||||
|
|
||||||
|
/// Absolute tolerance.
|
||||||
|
eps: 1e-20,
|
||||||
|
|
||||||
|
// Relative tolerance
|
||||||
|
rel-eps: 1e-09
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(a) == array and type(b) == array {
|
||||||
|
assert(a.len() == b.len(), message: "Non-matching array lengths " + repr(a) + " / " + repr(b))
|
||||||
|
for (x, y) in a.zip(b) {
|
||||||
|
// if calc.abs(x - y) >= eps and calc.abs(1 - calc.abs(x / y)) >= eps {
|
||||||
|
if calc.abs(x - y) > calc.max(rel-eps * calc.max(calc.abs(x), calc.abs(y)), eps) {
|
||||||
|
assert(false, message: repr(x) + " was not approx equal to " + repr(y) )
|
||||||
|
assert(false, message: repr(a) + " was not approx equal to " + repr(b) )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert(calc.abs(a - b) < eps, message: str(a) + " and " + str(b) + " are not equal up to " + str(eps))
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#approx(1, 1)
|
||||||
|
#approx((5e45, 1e46, 1.4999999999999999e46, 2e46), (5e45, 1e46, 1.5e46, 2e46))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Assert that coordinate arrays passed to plots have the same length.
|
||||||
|
#let assert-matching-data-dimensions(
|
||||||
|
|
||||||
|
/// Array of $x$ coordinates.
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Array of $y$ coordinates.
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Other coordinate arrays (e.g., error bars) that should match the length of $x$ and $y$ coordinates. Only named arguments are accepted.
|
||||||
|
..args,
|
||||||
|
|
||||||
|
/// Function name. This can be used to improve the error message. Entries where the value is not an array are ignored. This is useful for cases like `xerr` that also take a single value.
|
||||||
|
fn-name: "",
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let prefix = ""
|
||||||
|
if fn-name != "" { prefix = "`" + fn-name + "()`: " }
|
||||||
|
|
||||||
|
if y != none {
|
||||||
|
assert(
|
||||||
|
x.len() == y.len(),
|
||||||
|
message: prefix + "The dimensions for x (" + str(x.len()) + ") and y (" + str(y.len()) + ") don't match"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for (key, value) in args.named() {
|
||||||
|
if value == none { continue }
|
||||||
|
if type(value) != array { continue }
|
||||||
|
|
||||||
|
assert(
|
||||||
|
x.len() == value.len(),
|
||||||
|
message: prefix + "The dimensions for x (" + str(x.len()) + ") and " + key + " (" + str(value.len()) + ") don't match"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Assert that there are no additional named arguments in an argument sink.
|
||||||
|
#let assert-no-named(
|
||||||
|
|
||||||
|
/// Argument sink.
|
||||||
|
args,
|
||||||
|
|
||||||
|
/// Function name. This can be used to improve the error message.
|
||||||
|
fn: ""
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if args.named().len() == 0 { return }
|
||||||
|
|
||||||
|
assert(false,
|
||||||
|
message: "Unexpected named argument \"" + args.named().keys().first() + "\"" + if fn == "" {""} else {
|
||||||
|
" in function " + fn + "()"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Assert that there are no additional positional arguments in an argument sink.
|
||||||
|
#let assert-no-positional(
|
||||||
|
|
||||||
|
/// Argument sink.
|
||||||
|
args
|
||||||
|
|
||||||
|
) = assert.eq(
|
||||||
|
args.pos().len(),
|
||||||
|
0,
|
||||||
|
message: "Unexpected positional arguments"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let assert-dict-keys(
|
||||||
|
dict,
|
||||||
|
mandatory: (),
|
||||||
|
optional: (),
|
||||||
|
name: "",
|
||||||
|
message: auto,
|
||||||
|
missing-message: auto,
|
||||||
|
unexpected-message: auto
|
||||||
|
) = {
|
||||||
|
name += " "
|
||||||
|
for key in mandatory {
|
||||||
|
if key not in dict {
|
||||||
|
if message == auto {
|
||||||
|
message = name + "dictionary expects key `" + key + "`"
|
||||||
|
}
|
||||||
|
if type(missing-message) == function {
|
||||||
|
message = missing-message(key)
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let _ = dict.remove(key)
|
||||||
|
}
|
||||||
|
for key in dict.keys() {
|
||||||
|
if key not in optional {
|
||||||
|
|
||||||
|
if message == auto {
|
||||||
|
message = name + "dictionary found unexpected key `" + key + "` (expected " + (mandatory + optional).join(", ", last: ", or ") + ")"
|
||||||
|
}
|
||||||
|
if type(unexpected-message) == function {
|
||||||
|
message = unexpected-message(key, (mandatory + optional).join(", ", last: ", or "))
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: message
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// #assert-dict-keys((a: 12, b: "", c: "a"), mandatory: ("a", "b"), optional: ("c",))
|
||||||
190
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/bounds.typ
vendored
Normal file
190
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/bounds.typ
vendored
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
|
||||||
|
#let get-length(x, container-length) = {
|
||||||
|
if type(x) == length { return x }
|
||||||
|
if type(x) == ratio { return x * container-length}
|
||||||
|
if type(x) == relative { return x.length + x.ratio * container-length}
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(get-length(3cm, 234cm), 3cm)
|
||||||
|
#assert.eq(get-length(50%, 224cm), 112cm)
|
||||||
|
#assert.eq(get-length(50% + 3cm, 224cm), 115cm)
|
||||||
|
|
||||||
|
|
||||||
|
#let update-bounds(former, bounds, width: 0cm, height: 0cm) = (
|
||||||
|
left: calc.min(former.left, get-length(bounds.left, width).to-absolute()),
|
||||||
|
top: calc.min(former.top, get-length(bounds.top, height).to-absolute()),
|
||||||
|
right: calc.max(former.right, get-length(bounds.right, width).to-absolute()),
|
||||||
|
bottom: calc.max(former.bottom, get-length(bounds.bottom, height).to-absolute()),
|
||||||
|
)
|
||||||
|
|
||||||
|
#context assert.eq(update-bounds(
|
||||||
|
(left: 0pt, right: 0pt, top: 0pt, bottom: 0pt),
|
||||||
|
(left: 0pt, right: 0pt, top: 0pt, bottom: 0pt),
|
||||||
|
), (left: 0pt, right: 0pt, top: 0pt, bottom: 0pt))
|
||||||
|
|
||||||
|
#context assert.eq(update-bounds(
|
||||||
|
(left: 0pt, right: 0pt, top: 0pt, bottom: 0pt),
|
||||||
|
(left: -10pt, right: 20pt, top: -20pt, bottom: 100pt),
|
||||||
|
), (left: -10pt, right: 20pt, top: -20pt, bottom: 100pt))
|
||||||
|
|
||||||
|
#context assert.eq(update-bounds(
|
||||||
|
(left: 0pt, right: 0pt, top: 0pt, bottom: 0pt),
|
||||||
|
(left: -3em, right: 20pt + 1em, top: -20pt, bottom: 2em + 1pt),
|
||||||
|
), (left: -33pt, right: 31pt, top: -20pt, bottom: 23pt))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let create-bounds() = (left: 0pt, right: 0pt, top: 0pt, bottom: 0pt)
|
||||||
|
|
||||||
|
|
||||||
|
#let offset-bounds(bounds, offset) = (
|
||||||
|
left: bounds.left + offset.at(0),
|
||||||
|
top: bounds.top + offset.at(1),
|
||||||
|
right: bounds.right + offset.at(0),
|
||||||
|
bottom: bounds.bottom + offset.at(1),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#let place-with-bounds(
|
||||||
|
content,
|
||||||
|
dx: 0pt,
|
||||||
|
dy: 0pt,
|
||||||
|
pad: 0pt,
|
||||||
|
alignment: top + left,
|
||||||
|
content-alignment: auto,
|
||||||
|
wrap-in-box: false
|
||||||
|
) = {
|
||||||
|
if alignment.x == none { alignment = alignment.y + center }
|
||||||
|
else if alignment.y == none { alignment = alignment.x + horizon }
|
||||||
|
if content-alignment == auto { content-alignment = alignment.inv() }
|
||||||
|
else if content-alignment == "inside" { content-alignment = alignment }
|
||||||
|
|
||||||
|
let size = measure(content)
|
||||||
|
if pad != 0pt {
|
||||||
|
if type(pad) != dictionary {
|
||||||
|
pad = (x: pad, y: pad)
|
||||||
|
}
|
||||||
|
if content-alignment.y == bottom { pad.y *= -1 }
|
||||||
|
else if content-alignment.y == horizon { pad.y *= 0 }
|
||||||
|
dy += pad.y
|
||||||
|
if content-alignment.x == right { pad.x *= -1 }
|
||||||
|
else if content-alignment.x == center { pad.x *= 0 }
|
||||||
|
dx += pad.x
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let (ddx, ddy) = (dx, dy)
|
||||||
|
|
||||||
|
if wrap-in-box {
|
||||||
|
content = box(..size, content)
|
||||||
|
}
|
||||||
|
let content = place(content-alignment, content)
|
||||||
|
|
||||||
|
if alignment.x == right { dx += 100% }
|
||||||
|
else if alignment.x == center { dx += 50% }
|
||||||
|
if alignment.y == bottom { dy += 100% }
|
||||||
|
else if alignment.y == horizon { dy += 50% }
|
||||||
|
|
||||||
|
if content-alignment.x == right { dx -= size.width }
|
||||||
|
else if content-alignment.x == center { dx -= 0.5 * size.width }
|
||||||
|
if content-alignment.y == bottom { dy -= size.height }
|
||||||
|
else if content-alignment.y == horizon { dy -= 0.5 * size.height }
|
||||||
|
let bounds = (
|
||||||
|
left: dx,
|
||||||
|
right: dx + size.width,
|
||||||
|
top: dy,
|
||||||
|
bottom: dy + size.height
|
||||||
|
)
|
||||||
|
(place(alignment, content, dx: ddx, dy: ddy), bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let place-and-show-bounds(content, alignment, ca: auto, pad: 0pt) = context {
|
||||||
|
let ca = ca
|
||||||
|
if ca == auto { ca = alignment.inv() }
|
||||||
|
let (content, bounds) = place-with-bounds(alignment: alignment, content, content-alignment: ca, pad: pad)
|
||||||
|
|
||||||
|
place(dx: bounds.left, dy: bounds.top, box(width: bounds.right - bounds.left, height: bounds.bottom - bounds.top))
|
||||||
|
|
||||||
|
content
|
||||||
|
}
|
||||||
|
#place-and-show-bounds([JOO], top , ca: right)
|
||||||
|
|
||||||
|
#rect(
|
||||||
|
width: 6cm, height: 4cm, inset: 0pt,
|
||||||
|
{
|
||||||
|
set box(fill: green.lighten(50%))
|
||||||
|
place-and-show-bounds([Top right], right + top)
|
||||||
|
place-and-show-bounds([Bottom right], right + bottom)
|
||||||
|
place-and-show-bounds([Top left], left + top)
|
||||||
|
place-and-show-bounds([Bottom left], left + bottom)
|
||||||
|
place-and-show-bounds([Top], top + center)
|
||||||
|
place-and-show-bounds([Bottom], bottom + center)
|
||||||
|
place-and-show-bounds([Middle], horizon + center)
|
||||||
|
place-and-show-bounds([Right], horizon + right)
|
||||||
|
place-and-show-bounds([Left], horizon + left)
|
||||||
|
|
||||||
|
set box(fill: purple.lighten(50%))
|
||||||
|
place-and-show-bounds([Top right], right + top, ca: right + top)
|
||||||
|
place-and-show-bounds([Bottom right], right + bottom, ca: right + bottom)
|
||||||
|
place-and-show-bounds([Top left], left + top, ca: left + top)
|
||||||
|
place-and-show-bounds([Bottom left], left + bottom, ca: left + bottom)
|
||||||
|
place-and-show-bounds([Top], top + center, ca: top + center)
|
||||||
|
place-and-show-bounds([Bottom], bottom + center, ca: bottom + center)
|
||||||
|
place-and-show-bounds([Middle], horizon + center, ca: horizon + center)
|
||||||
|
place-and-show-bounds([Right], horizon + right, ca: horizon + right)
|
||||||
|
place-and-show-bounds([Left], horizon + left, ca: horizon + left)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
\
|
||||||
|
|
||||||
|
#rect(
|
||||||
|
width: 6cm, height: 4cm, inset: 0pt,
|
||||||
|
{
|
||||||
|
set box(fill: red.lighten(50%))
|
||||||
|
place-and-show-bounds([Top right], right + top, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Bottom right], right + bottom, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Top left], left + top, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Bottom left], left + bottom, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Top], top + center, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Bottom], bottom + center, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Middle], horizon + center, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Right], horizon + right, ca: center + horizon)
|
||||||
|
place-and-show-bounds([Left], horizon + left, ca: center + horizon)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
With auto padding
|
||||||
|
|
||||||
|
#rect(
|
||||||
|
width: 6cm, height: 4cm, inset: 0pt,
|
||||||
|
{
|
||||||
|
set box(fill: green.lighten(50%))
|
||||||
|
place-and-show-bounds = place-and-show-bounds.with(pad: 5pt)
|
||||||
|
|
||||||
|
place-and-show-bounds([Top right], right + top)
|
||||||
|
place-and-show-bounds([Bottom right], right + bottom)
|
||||||
|
place-and-show-bounds([Top left], left + top)
|
||||||
|
place-and-show-bounds([Bottom left], left + bottom)
|
||||||
|
place-and-show-bounds([Top], top + center)
|
||||||
|
place-and-show-bounds([Bottom], bottom + center)
|
||||||
|
place-and-show-bounds([Middle], horizon + center)
|
||||||
|
place-and-show-bounds([Right], horizon + right)
|
||||||
|
place-and-show-bounds([Left], horizon + left)
|
||||||
|
|
||||||
|
set box(fill: purple.lighten(50%))
|
||||||
|
place-and-show-bounds([Top right], right + top, ca: right + top)
|
||||||
|
place-and-show-bounds([Bottom right], right + bottom, ca: right + bottom)
|
||||||
|
place-and-show-bounds([Top left], left + top, ca: left + top)
|
||||||
|
place-and-show-bounds([Bottom left], left + bottom, ca: left + bottom)
|
||||||
|
place-and-show-bounds([Top], top + center, ca: top + center)
|
||||||
|
place-and-show-bounds([Bottom], bottom + center, ca: bottom + center)
|
||||||
|
place-and-show-bounds([Middle], horizon + center, ca: horizon + center)
|
||||||
|
place-and-show-bounds([Right], horizon + right, ca: horizon + right)
|
||||||
|
place-and-show-bounds([Left], horizon + left, ca: horizon + left)
|
||||||
|
place-and-show-bounds([JOO], top + right, ca: left)
|
||||||
|
}
|
||||||
|
)
|
||||||
51
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/lilaq.typ
vendored
Normal file
51
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/lilaq.typ
vendored
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
#import "math.typ": *
|
||||||
|
|
||||||
|
#import "model/diagram.typ": diagram
|
||||||
|
#import "model/axis.typ": axis, xaxis, yaxis
|
||||||
|
#import "model/tick.typ": tick, tick-label
|
||||||
|
#import "model/spine.typ": spine
|
||||||
|
#import "model/title.typ": title
|
||||||
|
#import "model/legend.typ": legend
|
||||||
|
#import "model/grid.typ": grid
|
||||||
|
#import "model/errorbar.typ": errorbar
|
||||||
|
#import "model/label.typ": label, xlabel, ylabel
|
||||||
|
|
||||||
|
#import "model/mark.typ": mark, marks
|
||||||
|
#import "style/styling.typ": style
|
||||||
|
#import "style/cycle.typ"
|
||||||
|
#import "style/color.typ"
|
||||||
|
|
||||||
|
#import "plot/plot.typ": plot
|
||||||
|
#import "plot/bar.typ": bar
|
||||||
|
#import "plot/hbar.typ": hbar
|
||||||
|
#import "plot/stem.typ": stem
|
||||||
|
#import "plot/hstem.typ": hstem
|
||||||
|
#import "plot/scatter.typ": scatter
|
||||||
|
#import "plot/fill-between.typ": fill-between
|
||||||
|
#import "plot/colormesh.typ": colormesh
|
||||||
|
#import "plot/contour.typ": contour
|
||||||
|
#import "plot/boxplot.typ": boxplot
|
||||||
|
#import "plot/hboxplot.typ": hboxplot
|
||||||
|
#import "plot/quiver.typ": quiver
|
||||||
|
#import "plot/hlines.typ": hlines
|
||||||
|
#import "plot/vlines.typ": vlines
|
||||||
|
|
||||||
|
#import "plot/rect.typ": rect
|
||||||
|
#import "plot/ellipse.typ": ellipse
|
||||||
|
#import "plot/line.typ": line
|
||||||
|
#import "plot/path.typ": path
|
||||||
|
#import "plot/place.typ": place
|
||||||
|
|
||||||
|
|
||||||
|
#import "style/tilings.typ"
|
||||||
|
#import "model/colorbar.typ": colorbar
|
||||||
|
#import "place-anchor.typ": place-anchor
|
||||||
|
|
||||||
|
|
||||||
|
#import "typing.typ": set-grid, set-label, set-title, set-legend, set-tick, set-tick-label, set-spine, set-diagram, set-errorbar, selector, fields, show_, cond-set
|
||||||
|
|
||||||
|
#import "logic/scale.typ"
|
||||||
|
#import "logic/tick-locate.typ" as tick-locate: linear as locate-ticks-linear, log as locate-ticks-log, symlog as locate-ticks-symlog, manual as locate-ticks-manual, subticks-linear as locate-subticks-linear, subticks-log as locate-subticks-log, subticks-symlog as locate-subticks-symlog, datetime as locate-ticks-datetime
|
||||||
|
#import "logic/tick-format.typ" as tick-format: linear as format-ticks-linear, log as format-ticks-log, manual as format-ticks-manual, symlog as format-ticks-symlog, datetime as format-ticks-datetime
|
||||||
|
|
||||||
|
#import "theme/theme.typ"
|
||||||
163
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/loading/txt.typ
vendored
Normal file
163
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/loading/txt.typ
vendored
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
|
||||||
|
/// Parses a CSV (comma-separated values) string. This function enhances the
|
||||||
|
/// functionality of the built-in Typst function [`csv`](https://typst.app/docs/reference/data-loading/csv/) with features like transforming
|
||||||
|
/// values to numerical (or other) types, ignoring comments and selecting only part of
|
||||||
|
/// the data.
|
||||||
|
///
|
||||||
|
/// Unlike the built-in `csv` function, this function returns the data as a list of
|
||||||
|
/// columns and not as a list of rows.
|
||||||
|
///
|
||||||
|
/// -> array | dictionary
|
||||||
|
#let load-txt(
|
||||||
|
|
||||||
|
/// Raw data loaded from a text file via [`read()`](https://typst.app/docs/reference/data-loading/read/).
|
||||||
|
/// -> str
|
||||||
|
data,
|
||||||
|
|
||||||
|
/// The delimiter that separates columns in the file.
|
||||||
|
/// -> str
|
||||||
|
delimiter: ",",
|
||||||
|
|
||||||
|
/// The characters that indicate the start of a single-line comment.
|
||||||
|
/// -> str
|
||||||
|
comments: "#",
|
||||||
|
|
||||||
|
/// The number of leading rows to be skipped, including comments.
|
||||||
|
/// -> int
|
||||||
|
skip-rows: 0,
|
||||||
|
|
||||||
|
/// Which columns to extract from the file. Expects an array of indices to columns to
|
||||||
|
/// extract. If `auto`, all columns are extracted.
|
||||||
|
/// -> auto | array
|
||||||
|
usecols: auto,
|
||||||
|
|
||||||
|
/// If true, the first line is interpreted as a header naming the individual columns.
|
||||||
|
/// The result is then returned as a dictionary with the headers.
|
||||||
|
/// -> bool
|
||||||
|
header: false,
|
||||||
|
|
||||||
|
/// Optional converter functions or types to use to convert the data entries. This
|
||||||
|
/// can either be a single function or type that is applied to all columns likewise
|
||||||
|
/// or a dictionary with column indices, or header names if enabled, as keys and functions or types as values.
|
||||||
|
/// Through the (optional) key `rest`, a default converter can be specified to be used
|
||||||
|
/// for all columns that have no explicit converter assigned.
|
||||||
|
/// -> function | type | dictionary
|
||||||
|
converters: float
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let rows = data.split("\n")
|
||||||
|
.slice(skip-rows)
|
||||||
|
.filter(row => not (row.starts-with(comments) or row == ""))
|
||||||
|
|
||||||
|
rows = rows.map(row => row.split(delimiter))
|
||||||
|
if rows.len() == 0 { return () }
|
||||||
|
|
||||||
|
let len = rows.first().len()
|
||||||
|
for (i, row) in rows.enumerate() {
|
||||||
|
assert(row.len() == len, message: "All rows need to be of the same length but the row " + repr(row.join(delimiter)) + " does not have " + str(len) + " entries as the other ones. ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if header {
|
||||||
|
header = rows.at(0).map(str.trim)
|
||||||
|
assert(header.dedup().len() == header.len(), message: "Duplicate entry in header")
|
||||||
|
rows = rows.slice(1)
|
||||||
|
}
|
||||||
|
let cols = array.zip(..rows)
|
||||||
|
if type(converters) == dictionary {
|
||||||
|
let default-converter = converters.at("rest", default: float)
|
||||||
|
|
||||||
|
if header != false {
|
||||||
|
for name in converters.keys() {
|
||||||
|
if name == "rest" { continue }
|
||||||
|
assert(name in header, message: "Found converter that doesn't map to a header: " + repr(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cols = range(cols.len()).map(j => {
|
||||||
|
let name = if header == false { str(j) } else { header.at(j) }
|
||||||
|
let converter = converters.at(name, default: default-converter)
|
||||||
|
cols.at(j).map(str.trim).map(converter)
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
assert(type(converters) in (function, type), message: "The converter needs to a function or a type")
|
||||||
|
cols = cols.map(col => col.map(str.trim).map(converters))
|
||||||
|
}
|
||||||
|
|
||||||
|
if header == false {
|
||||||
|
if usecols == auto { return cols }
|
||||||
|
if type(usecols) == int { usecols = (usecols,) }
|
||||||
|
assert(type(usecols) == array, message: "Parameter `usecols` expects an int or an array of ints")
|
||||||
|
return usecols.map(j => cols.at(j))
|
||||||
|
}
|
||||||
|
assert(usecols == auto, message: "Parameter `usecols` can not be used with `header: true`, You can just partially destructure the dictionary. ")
|
||||||
|
return header.zip(cols).to-dict()
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2,3\n4,5,6"),
|
||||||
|
((1,4), (2,5), (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1, 2 , 3 \n 4, 5, 6"),
|
||||||
|
((1,4), (2,5), (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("\n1,2,3\n4,5,6\n\n"),
|
||||||
|
((1,4), (2,5), (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("\n1 2 3\n4 5 6", delimiter: " "),
|
||||||
|
((1,4), (2,5), (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2,3\n//a\n4,5,6\n//a", comments: "//"),
|
||||||
|
((1,4), (2,5), (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("blablabla\n1,2\n3,4\n5,6", skip-rows: 1),
|
||||||
|
((1,3,5), (2,4,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt(" n, a, b\n1,2,3\n4,5,6", header: true),
|
||||||
|
(n: (1,4), a: (2,5), b: (3,6))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2,3\n4,5,6", usecols: 1),
|
||||||
|
((2,5),)
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2,3\n4,5,6", usecols: (0,2)),
|
||||||
|
((1,4), (3,6))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2\n4,5", converters: v => v),
|
||||||
|
(("1","4"), ("2","5"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2\n4,5", converters: ("0": v => v)),
|
||||||
|
(("1","4"), (2,5))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2\n4,5", converters: ("1": v => v)),
|
||||||
|
((1,4), ("2","5"))
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2\n4,5", converters: ("0": type, "1": v => v)),
|
||||||
|
((str, str), ("2","5"))
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt("1,2\n4,5", converters: ("0": float, rest: v => v)),
|
||||||
|
((1, 4), ("2","5"))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
load-txt(" n, a, b\n1,2,3\n4,5,6", header: true, converters: (n: v => v, a: int, rest: v => v)),
|
||||||
|
(n: ("1","4"), a: (2,5), b: ("3","6"))
|
||||||
|
)
|
||||||
28
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/limits.typ
vendored
Normal file
28
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/limits.typ
vendored
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "process-coordinates.typ": is-data-coordinates
|
||||||
|
|
||||||
|
#let plot-lim(x, err: none) = {
|
||||||
|
if err == none { return minmax(x) }
|
||||||
|
|
||||||
|
return (
|
||||||
|
calc.min(..array.zip(x, err.m).map(((x, err)) => x - if type(err) == array { err.at(0) } else { err })),
|
||||||
|
calc.max(..array.zip(x, err.p).map(((x, err)) => x + if type(err) == array { err.at(1) } else { err })),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let bar-lim(x, base) = {
|
||||||
|
let lim = minmax(x + base)
|
||||||
|
let (base-min, base-max) = minmax(base)
|
||||||
|
if lim.at(0) == base-min { lim.at(0) *= 1fr }
|
||||||
|
if lim.at(1) == base-max { lim.at(1) *= 1fr }
|
||||||
|
return lim
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignores (relative) lengths and ratios and
|
||||||
|
// only accounts for data coordinates (which are
|
||||||
|
// given as floats).
|
||||||
|
#let compute-primitive-limits(coords) = {
|
||||||
|
let filtered-coords = coords.filter(is-data-coordinates)
|
||||||
|
if filtered-coords.len() == 0 { return none }
|
||||||
|
return (calc.min(..filtered-coords), calc.max(..filtered-coords))
|
||||||
|
}
|
||||||
182
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/process-coordinates.typ
vendored
Normal file
182
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/process-coordinates.typ
vendored
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
|
||||||
|
/// Takes an array of points as input and filters all points where at least one
|
||||||
|
/// coordinate is `calc.nan` to produce
|
||||||
|
/// + a filtered copy of the input array
|
||||||
|
/// + an array of consecutive "runs", i.e., all connected sequences from the input
|
||||||
|
/// separated by points where one or more coordinates take the value `calc.nan`.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
#let filter-nan-points(
|
||||||
|
|
||||||
|
/// Input points. The points themselves may have any dimension.
|
||||||
|
/// -> array
|
||||||
|
points,
|
||||||
|
|
||||||
|
/// Whether to to split the data into separate runs whenever a coordinate
|
||||||
|
/// containing a `nan` value is encountered.
|
||||||
|
/// -> bool
|
||||||
|
generate-runs: false
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if generate-runs {
|
||||||
|
let filtered-points = ()
|
||||||
|
let runs = ((),)
|
||||||
|
for coord in points {
|
||||||
|
if coord.find(float.is-nan) != none {
|
||||||
|
if runs.last().len() != 0 {
|
||||||
|
runs.push(())
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered-points.push(coord)
|
||||||
|
runs.last().push(coord)
|
||||||
|
}
|
||||||
|
return (filtered-points, runs)
|
||||||
|
} else {
|
||||||
|
return points.filter(p => p.find(float.is-nan) == none)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Converts an array of points to a step sequence.
|
||||||
|
/// Given $n$ points, the output will have $2n-1$ points and zero points
|
||||||
|
/// if the input has zero points.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
#let stepify(
|
||||||
|
|
||||||
|
/// Input points of the form `(x, y)` with `nan` values removed.
|
||||||
|
/// -> array
|
||||||
|
points,
|
||||||
|
|
||||||
|
/// Step mode
|
||||||
|
/// - `start`: The interval $(x_{i-1}, x_i]$ takes the value of $x_i$.
|
||||||
|
/// - `end`: The interval $[x_i, x_{i+1})$ takes the value of $x_i$.
|
||||||
|
/// - `center`: The value switches half-way between consecutive $x$ positions.
|
||||||
|
/// -> start | center | end
|
||||||
|
step: start
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if points.len() == 0 { return () }
|
||||||
|
let result = ()
|
||||||
|
if step == start {
|
||||||
|
for i in range(points.len() - 1) {
|
||||||
|
result.push(points.at(i))
|
||||||
|
result.push((points.at(i).at(0), points.at(i + 1).at(1)))
|
||||||
|
}
|
||||||
|
} else if step == end {
|
||||||
|
for i in range(points.len() - 1) {
|
||||||
|
result.push(points.at(i))
|
||||||
|
result.push((points.at(i + 1).at(0), points.at(i).at(1)))
|
||||||
|
}
|
||||||
|
} else if step == center {
|
||||||
|
for i in range(points.len() - 1) {
|
||||||
|
result.push(points.at(i))
|
||||||
|
let mid = 0.5 * (points.at(i).at(0) + points.at(i + 1).at(0))
|
||||||
|
result.push((mid, points.at(i).at(1)))
|
||||||
|
result.push((mid, points.at(i + 1).at(1)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push(points.last())
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(stepify((), step: start), ())
|
||||||
|
#assert.eq(stepify(((0,0),), step: start), ((0,0),))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7)), step: start), ((0,0), (0,.7), (1,.7)))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7), (3,-.1)), step: start), ((0,0), (0,.7), (1,.7), (1,-.1), (3,-.1)))
|
||||||
|
|
||||||
|
#assert.eq(stepify((), step: end), ())
|
||||||
|
#assert.eq(stepify(((0,0),), step: end), ((0,0),))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7)), step: end), ((0,0), (1,0), (1,.7)))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7), (3,-.1)), step: end), ((0,0), (1,0), (1,.7), (3, .7), (3,-.1)))
|
||||||
|
|
||||||
|
#assert.eq(stepify((), step: center), ())
|
||||||
|
#assert.eq(stepify(((0,0),), step: center), ((0,0),))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7)), step: center), ((0,0), (0.5,0), (0.5, .7), (1,.7)))
|
||||||
|
#assert.eq(stepify(((0,0), (1,.7), (3,-.1)), step: center), ((0,0), (.5,0), (.5, .7), (1,.7), (2, .7), (2, -.1), (3,-.1)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Transforms a generalized point
|
||||||
|
#let transform-point(x, y, transform) = {
|
||||||
|
if type(x) in (int, float) {
|
||||||
|
x = transform(x, 1).at(0)
|
||||||
|
}
|
||||||
|
if type(y) in (int, float) {
|
||||||
|
y = transform(1, y).at(1)
|
||||||
|
}
|
||||||
|
return (x, y)
|
||||||
|
}
|
||||||
|
#let convert-bezier-curve(points, transform) = {
|
||||||
|
let v = points.at(0)
|
||||||
|
let p = transform-point(..v, transform)
|
||||||
|
let result = (p,)
|
||||||
|
|
||||||
|
for (x, y) in points.slice(1) {
|
||||||
|
if type(x) in (int, float) {
|
||||||
|
x = transform(x + v.at(0), 1).at(0) - p.at(0)
|
||||||
|
}
|
||||||
|
if type(y) in (int, float) {
|
||||||
|
y = transform(1, y + v.at(1)).at(1) - p.at(1)
|
||||||
|
}
|
||||||
|
result.push((x, y))
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// point and size pt -> makes sense
|
||||||
|
// point and size data coords -> makes sense
|
||||||
|
// point data coords and size pt -> makes sense
|
||||||
|
// point data pt and size data coords -> makes no sense
|
||||||
|
|
||||||
|
|
||||||
|
#let convert-rect(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
transform,
|
||||||
|
align: top + left
|
||||||
|
) = {
|
||||||
|
|
||||||
|
// at the end we only want (relative) lengths
|
||||||
|
|
||||||
|
let (x1, y1) = transform-point(x, y, transform)
|
||||||
|
if type(width) in (int, float) {
|
||||||
|
assert(type(x) in (int, float), message: "Setting the width in terms of data coordinates is only allowed if the origin x coordinate is given in data coordinates")
|
||||||
|
width = transform(x + width, 1).at(0) - transform(x, 1).at(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(height) in (int, float) {
|
||||||
|
assert(type(y) in (int, float), message: "Setting the height in terms of data coordinates is only allowed if the origin y coordinate is given in data coordinates")
|
||||||
|
height = transform(1, y + height).at(1) - transform(1, y).at(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if align.x == right { x1 -= width }
|
||||||
|
else if align.x == center { x1 -= width / 2 }
|
||||||
|
|
||||||
|
if align.y == bottom { y1 -= height }
|
||||||
|
else if align.y == horizon { y1 -= height / 2 }
|
||||||
|
|
||||||
|
|
||||||
|
return (x1, width, y1, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let is-data-coordinates(coord) = type(coord) in (int, float)
|
||||||
|
#let all-data-coordinates(coords) = {
|
||||||
|
return coords.map(is-data-coordinates).fold(true, (a, b) => a and b)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Convert a vertex for `path`. A vertex may either be a single vertex
|
||||||
|
// or a pair/triple of vertices describing a point with handles on a
|
||||||
|
// bezier curve.
|
||||||
|
#let convert-vertex(v, transform: it => it) = {
|
||||||
|
if type(v.at(0)) == array {
|
||||||
|
return v.map(p => transform-point(..p, transform))
|
||||||
|
} else {
|
||||||
|
transform-point(..v, transform)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
58
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/sample-colors.typ
vendored
Normal file
58
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/sample-colors.typ
vendored
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
|
||||||
|
#import "../utility.typ": match-type
|
||||||
|
#import "../math.typ" as pmath
|
||||||
|
#import "../logic/scale.typ"
|
||||||
|
#import "../logic/transform.typ": create-trafo
|
||||||
|
|
||||||
|
|
||||||
|
#let sample-colors(
|
||||||
|
values,
|
||||||
|
colormap,
|
||||||
|
norm,
|
||||||
|
min: auto,
|
||||||
|
max: auto,
|
||||||
|
ignore-nan: false,
|
||||||
|
excess: "clamp" // "clamp" | "mask"
|
||||||
|
) = {
|
||||||
|
if ignore-nan {
|
||||||
|
if min == auto { min = pmath.cmin(values) }
|
||||||
|
if max == auto { max = pmath.cmax(values) }
|
||||||
|
} else {
|
||||||
|
if min == auto { min = calc.min(..values) }
|
||||||
|
if max == auto { max = calc.max(..values) }
|
||||||
|
}
|
||||||
|
if min == max { min -= 1; max += 1}
|
||||||
|
|
||||||
|
|
||||||
|
if excess == "mask" {
|
||||||
|
values = values.map(v => if v < min or v > max { float.nan } else { v })
|
||||||
|
} else if excess == "clamp" {
|
||||||
|
// values = values.map(v => if v < min { min } else if v > max { max } else { v })
|
||||||
|
}
|
||||||
|
|
||||||
|
let norm-fn = match-type(
|
||||||
|
norm,
|
||||||
|
function: () => norm,
|
||||||
|
string: () => scale.scales.at(norm).transform,
|
||||||
|
dictionary: () => {
|
||||||
|
assert("transform" in norm, message: "The argument `norm` must be a valid scale from the `scales` module")
|
||||||
|
norm.transform
|
||||||
|
},
|
||||||
|
default: () => assert(false, message: "Unsupported type `" + str(type(norm)) + "` for argument `norm`")
|
||||||
|
)
|
||||||
|
|
||||||
|
let normalize = create-trafo(norm-fn, min, max)
|
||||||
|
assert(type(colormap) in (gradient, array), message: "Invalid type for colormap")
|
||||||
|
if type(colormap) == array {
|
||||||
|
colormap = gradient.linear(..colormap)
|
||||||
|
}
|
||||||
|
let convert-scalar-to-color(x) = {
|
||||||
|
if float.is-nan(x) { return luma(0, 0%) }
|
||||||
|
colormap.sample(normalize(x) * 100%)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
values.map(convert-scalar-to-color),
|
||||||
|
(norm: norm, min: min, max: max, colormap: colormap)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
174
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/scale.typ
vendored
Normal file
174
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/scale.typ
vendored
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
/// Scales that can be applied to the displayed data. By default, the
|
||||||
|
/// data is scaled linearly but other scales like `lq.scale.log` and
|
||||||
|
/// `lq.scale.symlog` are available and completely custom scales can
|
||||||
|
/// be created with the general `scale` function.
|
||||||
|
|
||||||
|
|
||||||
|
#import "../math.typ": sign
|
||||||
|
#import "../logic/tick-locate.typ"
|
||||||
|
|
||||||
|
/// Constructor for the scale type. Scales are used to transform data coordinates
|
||||||
|
/// into scaled coordinates. Commonly, data is displayed with a linear scale, i.e.,
|
||||||
|
/// the entire data is just scaled uniformly. However, in order to visualize large
|
||||||
|
/// ranges, it is often desirable to use logarithmic scaling or other scales that
|
||||||
|
/// improve the readability of the data.
|
||||||
|
#let scale(
|
||||||
|
|
||||||
|
/// Transformation from data coordinates to scaled coordinates.
|
||||||
|
/// Note that the transformation function does not need to worry about
|
||||||
|
/// absolute scaling and offsets. As an example, the transformation function
|
||||||
|
/// for the linear scale is just `x => x` and not something like
|
||||||
|
/// `x => a * x + b`. The logarithmic scale uses `x => calc.log(x)`.
|
||||||
|
/// -> function
|
||||||
|
transform,
|
||||||
|
|
||||||
|
/// A precise inverse of the `transform` in order to enable the conversion of
|
||||||
|
/// scaled coordinates back to data coordinates.
|
||||||
|
/// -> function
|
||||||
|
inverse,
|
||||||
|
|
||||||
|
/// Name of the scale. Built-in scales are sometimes identified by their name,
|
||||||
|
/// e.g., when a suitable tick locator needs to be selected automatically.
|
||||||
|
/// -> str
|
||||||
|
name: "",
|
||||||
|
|
||||||
|
/// An identity value which can be used to find an initial axis range when
|
||||||
|
/// no limits or plots are given. Scales like logarithmic scales that are
|
||||||
|
/// only defined for positive values should set this to 1.
|
||||||
|
/// -> int | float
|
||||||
|
identity: 0,
|
||||||
|
|
||||||
|
/// The default tick locator to use with this scale.
|
||||||
|
/// -> none | function
|
||||||
|
locate-ticks: none,
|
||||||
|
|
||||||
|
/// The default subtick locator to use with this scale.
|
||||||
|
/// -> none | function
|
||||||
|
locate-subticks: none,
|
||||||
|
|
||||||
|
/// Additional data to store in the scale.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = (
|
||||||
|
transform: transform,
|
||||||
|
inverse: inverse,
|
||||||
|
name: name,
|
||||||
|
identity: identity,
|
||||||
|
locate-ticks: locate-ticks,
|
||||||
|
locate-subticks: locate-subticks,
|
||||||
|
..args.named()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a new linear scale. This scale can also be accessed through
|
||||||
|
/// the shorthand `"linear"`.
|
||||||
|
#let linear() = scale(
|
||||||
|
name: "linear",
|
||||||
|
x => x,
|
||||||
|
x => x,
|
||||||
|
locate-ticks: tick-locate.linear,
|
||||||
|
locate-subticks: tick-locate.subticks-linear,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a new logarithmic scale. This scale can also be accessed through
|
||||||
|
/// the shorthand `"log"`.
|
||||||
|
#let log(
|
||||||
|
|
||||||
|
/// The base of the logarithm. This info is only used to determine
|
||||||
|
/// the base for ticks.
|
||||||
|
/// -> float
|
||||||
|
base: 10
|
||||||
|
|
||||||
|
) = scale(
|
||||||
|
name: "log",
|
||||||
|
x => calc.log(x),
|
||||||
|
x => calc.pow(10., x),
|
||||||
|
base: base,
|
||||||
|
identity: 1,
|
||||||
|
locate-ticks: tick-locate.log.with(base: base),
|
||||||
|
locate-subticks: tick-locate.subticks-log.with(base: base),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a new symlog scale with a linear scaling in the region
|
||||||
|
/// `[threshold, threshold]` around 0 and a logarithmic scaling beyond that.
|
||||||
|
/// This scale can also be accessed through the shorthand `"symlog"`.
|
||||||
|
#let symlog(
|
||||||
|
|
||||||
|
/// The base of the logarithm.
|
||||||
|
/// -> float
|
||||||
|
base: 10,
|
||||||
|
|
||||||
|
/// The threshold for the linear region.
|
||||||
|
/// -> float
|
||||||
|
threshold: 1,
|
||||||
|
|
||||||
|
/// The scaling of the linear region.
|
||||||
|
/// -> float
|
||||||
|
linscale: 1
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let c = linscale / (1. - 1. / base)
|
||||||
|
|
||||||
|
import "symlog.typ": symlog-transform
|
||||||
|
let transform = symlog-transform(base, threshold, linscale)
|
||||||
|
|
||||||
|
let inv-threshold = transform(threshold)
|
||||||
|
scale(
|
||||||
|
name: "symlog",
|
||||||
|
transform,
|
||||||
|
x => {
|
||||||
|
if x == 0 { return 0. }
|
||||||
|
let abs = calc.abs(x)
|
||||||
|
if abs <= inv-threshold { return x / c }
|
||||||
|
return sign(x) * threshold * calc.pow(base, abs / threshold - c)
|
||||||
|
},
|
||||||
|
|
||||||
|
threshold: threshold,
|
||||||
|
base: base,
|
||||||
|
linscale: linscale,
|
||||||
|
locate-ticks: tick-locate.symlog.with(
|
||||||
|
base: base,
|
||||||
|
threshold: threshold,
|
||||||
|
linscale: linscale
|
||||||
|
),
|
||||||
|
locate-subticks: tick-locate.subticks-symlog.with(
|
||||||
|
base: base,
|
||||||
|
threshold: threshold
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let check-sym(x) = {
|
||||||
|
let sym = symlog()
|
||||||
|
assert((sym.inverse)((sym.transform)(x)) - x < 1e-15)
|
||||||
|
}
|
||||||
|
#check-sym(0)
|
||||||
|
#check-sym(1)
|
||||||
|
#check-sym(1.5)
|
||||||
|
#check-sym(2)
|
||||||
|
#check-sym(3)
|
||||||
|
|
||||||
|
/// A linear scale with a datetime tick locator. This scale can also be
|
||||||
|
/// accessed through the shorthand "datetime".
|
||||||
|
#let datetime() = {
|
||||||
|
scale(
|
||||||
|
name: "datetime",
|
||||||
|
x => x,
|
||||||
|
x => x,
|
||||||
|
locate-ticks: tick-locate.datetime,
|
||||||
|
locate-subticks: none,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let scales = (
|
||||||
|
linear: linear(),
|
||||||
|
log: log(base: 10),
|
||||||
|
symlog: symlog(base: 10),
|
||||||
|
datetime: datetime()
|
||||||
|
)
|
||||||
|
|
||||||
14
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/symlog.typ
vendored
Normal file
14
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/symlog.typ
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
#import "../math.typ": sign
|
||||||
|
|
||||||
|
|
||||||
|
#let symlog-transform(base, threshold, linscale) = {
|
||||||
|
let c = linscale / (1. - 1. / base)
|
||||||
|
let log-base = calc.ln(base)
|
||||||
|
|
||||||
|
x => {
|
||||||
|
if x == 0 { return 0. }
|
||||||
|
let abs = calc.abs(x)
|
||||||
|
if abs <= threshold { return x * c }
|
||||||
|
return sign(x) * threshold * (c + calc.ln(abs / threshold) / log-base)
|
||||||
|
}
|
||||||
|
}
|
||||||
618
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/tick-format.typ
vendored
Normal file
618
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/tick-format.typ
vendored
Normal file
@@ -0,0 +1,618 @@
|
|||||||
|
#import "@preview/zero:0.5.0"
|
||||||
|
#import "../math.typ": pow10
|
||||||
|
#import "tick-locate.typ": _estimate-significant-digits
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats ticks with explicit labels. See @tick-locate.manual.
|
||||||
|
#let manual(
|
||||||
|
|
||||||
|
/// The ticks to format
|
||||||
|
/// -> array
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// Additional information from the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-info: (:),
|
||||||
|
|
||||||
|
/// Arguments that are ignored by this formatter.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assert(
|
||||||
|
"labels" in tick-info, message: "No labels given to `tick-format.manual`"
|
||||||
|
)
|
||||||
|
|
||||||
|
tick-info.labels
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let naive(
|
||||||
|
ticks,
|
||||||
|
..args
|
||||||
|
) = ticks.map(str)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let num(
|
||||||
|
value,
|
||||||
|
sign: 1,
|
||||||
|
e: none,
|
||||||
|
auto-e: true,
|
||||||
|
digits: auto,
|
||||||
|
base: 10,
|
||||||
|
omit-unit-mantissa: true
|
||||||
|
) = {
|
||||||
|
if digits != auto {
|
||||||
|
digits = calc.max(0, digits)
|
||||||
|
}
|
||||||
|
if e == 0 and auto-e { e = none }
|
||||||
|
if e != none {
|
||||||
|
e = str(e)
|
||||||
|
}
|
||||||
|
zero.num(
|
||||||
|
(
|
||||||
|
mantissa: str(value).replace(sym.minus, "-"),
|
||||||
|
pm: none,
|
||||||
|
e: e
|
||||||
|
),
|
||||||
|
base: base,
|
||||||
|
digits: digits,
|
||||||
|
omit-unity-mantissa: omit-unit-mantissa
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let default-generate-tick-label(
|
||||||
|
value,
|
||||||
|
exponent: 0,
|
||||||
|
digits: auto,
|
||||||
|
simple: false
|
||||||
|
) = {
|
||||||
|
if simple {
|
||||||
|
if exponent == 0 { return $#value$ }
|
||||||
|
return $#value dot 10^#exponent$
|
||||||
|
}
|
||||||
|
if exponent == 0 { exponent = none }
|
||||||
|
return num(value, e: exponent, digits: digits)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats linear ticks, see @tick-locate.linear. This is the most common tick
|
||||||
|
/// formatter.
|
||||||
|
#let linear(
|
||||||
|
|
||||||
|
/// The ticks to format.
|
||||||
|
/// -> array.
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// The exponent to apply to the ticks.
|
||||||
|
/// -> auto | int | "inline"
|
||||||
|
exponent: auto,
|
||||||
|
|
||||||
|
/// The offset to apply to the ticks.
|
||||||
|
/// -> auto | int | float
|
||||||
|
offset: auto,
|
||||||
|
|
||||||
|
/// Determines the threshold for automatic exponents to kick in.
|
||||||
|
/// -> int
|
||||||
|
auto-exponent-threshold: 3,
|
||||||
|
|
||||||
|
/// A suffix to display with each tick label.
|
||||||
|
/// -> content
|
||||||
|
suffix: none,
|
||||||
|
|
||||||
|
/// Additional information from the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-info: (:),
|
||||||
|
|
||||||
|
/// Arguments that are ignored by this formatter.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
|
||||||
|
let unit = tick-info.at("unit", default: 1)
|
||||||
|
|
||||||
|
if offset == auto {
|
||||||
|
offset = tick-info.at("offset", default: 0)
|
||||||
|
} else {
|
||||||
|
assert(type(offset) in (int, float), message: "Offsets need to be of integer or float type")
|
||||||
|
}
|
||||||
|
|
||||||
|
let additional-exponent = 0 // extra exp that will be shown on the axis
|
||||||
|
let inline-exponent = 0 // extra exp that is shown for each tick
|
||||||
|
|
||||||
|
let inherited-exponent = tick-info.at("exponent", default: 0)
|
||||||
|
|
||||||
|
if exponent == none { exponent = 0 }
|
||||||
|
if exponent == auto {
|
||||||
|
if calc.abs(inherited-exponent) >= auto-exponent-threshold {
|
||||||
|
additional-exponent = inherited-exponent
|
||||||
|
}
|
||||||
|
} else if exponent == "inline" {
|
||||||
|
if calc.abs(inherited-exponent) >= auto-exponent-threshold {
|
||||||
|
inline-exponent = inherited-exponent
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
assert.eq(type(exponent), int, message: "Exponents need to be of integer type")
|
||||||
|
additional-exponent = exponent
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let preapplied-exponent = inline-exponent + additional-exponent
|
||||||
|
let preapplied-factor = 1 / pow10(preapplied-exponent) / unit
|
||||||
|
let ticks = ticks.map(x => (x - offset*unit) * preapplied-factor)
|
||||||
|
|
||||||
|
|
||||||
|
let significant-digits = tick-info.at("significant-digits", default: none)
|
||||||
|
if significant-digits == none {
|
||||||
|
significant-digits = _estimate-significant-digits(ticks)
|
||||||
|
} else {
|
||||||
|
significant-digits += preapplied-exponent
|
||||||
|
}
|
||||||
|
|
||||||
|
let labels = ticks
|
||||||
|
.map(calc.round.with(digits: significant-digits))
|
||||||
|
.map(num.with(
|
||||||
|
e: inline-exponent,
|
||||||
|
digits: significant-digits
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if suffix != none {
|
||||||
|
labels = ticks.zip(labels).map(((tick, label)) => {
|
||||||
|
if tick == 0 { return $0$ }
|
||||||
|
tick = calc.round(tick, digits: 3)
|
||||||
|
|
||||||
|
if tick == 1 { label = none }
|
||||||
|
else if tick == -1 { label = "−"}
|
||||||
|
|
||||||
|
label + suffix
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
labels: labels,
|
||||||
|
exponent: additional-exponent,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats logarithmic ticks, see @tick-locate.log.
|
||||||
|
#let log(
|
||||||
|
|
||||||
|
/// The ticks to format.
|
||||||
|
/// -> array.
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// The base of the logarithm.
|
||||||
|
/// -> int | float
|
||||||
|
base: 10,
|
||||||
|
|
||||||
|
/// Whether to use a fixed exponent and which one.
|
||||||
|
/// -> auto | int
|
||||||
|
exponent: auto,
|
||||||
|
|
||||||
|
auto-exponent-threshold: 3,
|
||||||
|
round-exponent-digits: 4,
|
||||||
|
|
||||||
|
/// Which base to display with the ticks. If `auto`, the base is inferred
|
||||||
|
/// from `base`.
|
||||||
|
/// -> auto | content
|
||||||
|
base-label: auto,
|
||||||
|
|
||||||
|
/// Additional information from the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-info: (:),
|
||||||
|
|
||||||
|
/// Arguments that are ignored by this formatter.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
// Sometimes the log ticker resorts to linear ticking and then this is better
|
||||||
|
if "linear" in tick-info and tick-info.linear {
|
||||||
|
return linear(
|
||||||
|
ticks,
|
||||||
|
tick-info: tick-info,
|
||||||
|
exponent: exponent,
|
||||||
|
auto-exponent-threshold, auto-exponent-threshold
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if base-label == auto {
|
||||||
|
if base == calc.e { base-label = $e$}
|
||||||
|
else { base-label = base }
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if exponent == auto {
|
||||||
|
let num = num.with(omit-unit-mantissa: true, base: base-label, auto-e: false)
|
||||||
|
|
||||||
|
ticks.map(x =>
|
||||||
|
num(
|
||||||
|
float.signum(x),
|
||||||
|
e: calc.round(calc.log(calc.abs(x), base: base),
|
||||||
|
digits: round-exponent-digits)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
ticks.map(num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats symlog ticks, see @tick-locate.symlog.
|
||||||
|
#let symlog(
|
||||||
|
|
||||||
|
/// The ticks to format.
|
||||||
|
/// -> array.
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// The base of the logarithm.
|
||||||
|
/// -> int | float
|
||||||
|
base: 10,
|
||||||
|
|
||||||
|
/// The threshold where the scale switches between linear and logarithmic.
|
||||||
|
/// -> float
|
||||||
|
threshold: 1,
|
||||||
|
|
||||||
|
/// Whether to use a fixed exponent and which one.
|
||||||
|
/// -> auto | int
|
||||||
|
exponent: auto,
|
||||||
|
|
||||||
|
auto-exponent-threshold: 3,
|
||||||
|
round-exponent-digits: 4,
|
||||||
|
|
||||||
|
/// Which base to display with the ticks. If `auto`, the base is inferred
|
||||||
|
/// from `base`.
|
||||||
|
/// -> auto | content
|
||||||
|
base-label: auto,
|
||||||
|
|
||||||
|
/// Additional information from the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-info: (:),
|
||||||
|
|
||||||
|
/// Arguments that are ignored by this formatter.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
|
||||||
|
let upper-log = ticks.filter(tick => tick >= threshold or tick <= -threshold)
|
||||||
|
|
||||||
|
let log = log(
|
||||||
|
upper-log,
|
||||||
|
tick-info: tick-info,
|
||||||
|
base: base,
|
||||||
|
base-label: base-label,
|
||||||
|
exponent: exponent,
|
||||||
|
auto-exponent-threshold, auto-exponent-threshold
|
||||||
|
)
|
||||||
|
|
||||||
|
let linear = linear(
|
||||||
|
ticks.filter(tick => tick < threshold and tick > -threshold)
|
||||||
|
)
|
||||||
|
|
||||||
|
log + linear.labels
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Displays a smart first of a period (month, day, hour, minute, or second).
|
||||||
|
#let datetime-smart-first(
|
||||||
|
|
||||||
|
/// The time to display.
|
||||||
|
/// -> datetime
|
||||||
|
time,
|
||||||
|
|
||||||
|
/// Which first to display, e.g., `"month"` for the first month of a year.
|
||||||
|
/// -> str
|
||||||
|
period: "month",
|
||||||
|
|
||||||
|
/// What to display for the first month in a year.
|
||||||
|
/// -> str | function
|
||||||
|
month: "[year]",
|
||||||
|
|
||||||
|
/// What to display for the first day in a month.
|
||||||
|
/// -> str | function
|
||||||
|
day: "[month repr:short]",
|
||||||
|
|
||||||
|
/// What to display for the first hour of a day.
|
||||||
|
/// -> str | function
|
||||||
|
hour: "[month repr:short]-[day]",
|
||||||
|
|
||||||
|
/// What to display for the first minute of an hour.
|
||||||
|
/// -> str | function
|
||||||
|
minute: "[month repr:short]-[day]",
|
||||||
|
|
||||||
|
/// What to display for the first second of a minute.
|
||||||
|
/// -> str | function
|
||||||
|
second: "[month repr:short]-[day]",
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
#let datetime-smart-first = e.element.declare(
|
||||||
|
"datetime-smart-first",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
let format = it.at(it.period)
|
||||||
|
if type(format) == str { it.time.display(format) }
|
||||||
|
else { format(it.time) }
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("time", datetime, required: true),
|
||||||
|
e.field("period", str, default: "month"),
|
||||||
|
e.field("month", e.types.union(str, function), default: "[year]"),
|
||||||
|
e.field("day", e.types.union(str, function), default: "[month repr:short]"),
|
||||||
|
e.field("hour", e.types.union(str, function), default: "[month repr:short]-[day]"),
|
||||||
|
e.field("minute", e.types.union(str, function), default: "[month repr:short]-[day]"),
|
||||||
|
e.field("second", e.types.union(str, function), default: "[month repr:short]-[day]"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
/// Displays a datetime tick.
|
||||||
|
#let datetime-smart-format(
|
||||||
|
|
||||||
|
/// The date/time to display.
|
||||||
|
/// -> datetime
|
||||||
|
time,
|
||||||
|
|
||||||
|
/// Whether to use @datetime-smart-first for first instances in a period.
|
||||||
|
/// -> bool
|
||||||
|
smart-first: true,
|
||||||
|
|
||||||
|
/// The smallest changing period type between consecutive ticks.
|
||||||
|
/// -> str
|
||||||
|
period: "month",
|
||||||
|
|
||||||
|
/// How to display years.
|
||||||
|
/// -> str | function
|
||||||
|
year: "[year]",
|
||||||
|
|
||||||
|
/// How to display months.
|
||||||
|
/// -> str | function
|
||||||
|
month: "[month repr:short]",
|
||||||
|
|
||||||
|
/// How to display days.
|
||||||
|
/// -> str | function
|
||||||
|
day: "[day]",
|
||||||
|
|
||||||
|
/// How to display hours.
|
||||||
|
/// -> str | function
|
||||||
|
hour: "[hour]:[minute]",
|
||||||
|
|
||||||
|
/// How to display minutes.
|
||||||
|
/// -> str | function
|
||||||
|
minute: "[hour]:[minute]",
|
||||||
|
|
||||||
|
/// How to display seconds.
|
||||||
|
/// -> str | function
|
||||||
|
second: "[hour]:[minute]:[second]",
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let datetime-smart-format = e.element.declare(
|
||||||
|
"datetime-smart-format",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
|
||||||
|
if it.period == none {
|
||||||
|
return it.datetime.display()
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = if it.period in ("month", "day") { 1 } else { 0 }
|
||||||
|
|
||||||
|
let component = (
|
||||||
|
"year": dt => false,
|
||||||
|
"month": dt => dt.month() == 1,
|
||||||
|
"day": dt => dt.day() == 1,
|
||||||
|
"hour": dt => dt.hour() == 0,
|
||||||
|
"minute": dt => dt.hour() == 0 and dt.minute() == 0,
|
||||||
|
"second": dt => dt.hour() == 0 and dt.minute() == 0 and dt.second() == 0,
|
||||||
|
).at(it.period)
|
||||||
|
|
||||||
|
if it.smart-first and component(it.datetime) and it.period != "year" {
|
||||||
|
datetime-smart-first(it.datetime, period: it.period)
|
||||||
|
} else {
|
||||||
|
let format = it.at(it.period)
|
||||||
|
if type(format) == str { it.datetime.display(format) }
|
||||||
|
else { format(it.datetime) }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("datetime", datetime, required: true),
|
||||||
|
e.field("smart-first", bool, default: true),
|
||||||
|
e.field("period", e.types.option(str), default: "month"),
|
||||||
|
e.field("year", e.types.union(str, function), default: "[year]"),
|
||||||
|
e.field("month", e.types.union(str, function), default: "[month repr:short]"),
|
||||||
|
e.field("day", e.types.union(str, function), default: "[day]"),
|
||||||
|
e.field("hour", e.types.union(str, function), default: "[hour]:[minute]"),
|
||||||
|
e.field("minute", e.types.union(str, function), default: "[hour]:[minute]"),
|
||||||
|
e.field("second", e.types.union(str, function), default: "[hour]:[minute]:[second]"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let display-datetime-smart-offset = (it, smart-first: true) => {
|
||||||
|
|
||||||
|
if it.period == none {
|
||||||
|
return none
|
||||||
|
}
|
||||||
|
|
||||||
|
let period(datetime, period) = {
|
||||||
|
let format = it.at(period)
|
||||||
|
if type(format) == str { datetime.display(format) }
|
||||||
|
else { format(datetime) }
|
||||||
|
}
|
||||||
|
|
||||||
|
let first = it.ticks.first()
|
||||||
|
let last = it.ticks.last()
|
||||||
|
|
||||||
|
if it.period == "month" {
|
||||||
|
|
||||||
|
let has-no-first = first.month() != 1 or not smart-first
|
||||||
|
if (first.year() == last.year() and has-no-first) or not it.avoid-redundant {
|
||||||
|
period(first, "year")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if it.period == "day" {
|
||||||
|
|
||||||
|
let has-no-first = first.day() != 1 or not smart-first
|
||||||
|
if (first.year() == last.year() and first.month() == last.month() and has-no-first) or not it.avoid-redundant {
|
||||||
|
period(first, "month")
|
||||||
|
} else if first.year() == last.year() {
|
||||||
|
period(first, "year")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if it.period in ("hour", "minute", "second") {
|
||||||
|
|
||||||
|
if first.year() == 0 { return }
|
||||||
|
|
||||||
|
let has-no-first = first.hour() != 0 or not smart-first
|
||||||
|
if (first.year() == last.year() and first.month() == last.month() and first.day() == last.day() and has-no-first) or not it.avoid-redundant {
|
||||||
|
period(first, "day")
|
||||||
|
} else if first.year() == last.year() {
|
||||||
|
period(first, "year")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Displays an offset for a set of datetime ticks.
|
||||||
|
#let datetime-smart-offset(
|
||||||
|
|
||||||
|
/// The ticks as `datetime` instances.
|
||||||
|
/// -> array
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// Whether to avoid redundant information between ticks and offset.
|
||||||
|
/// For example, if the year is already displayed as a @datetime.smart-fist,
|
||||||
|
/// it is omitted here.
|
||||||
|
/// -> bool
|
||||||
|
avoid-redundant: true,
|
||||||
|
|
||||||
|
/// The smallest changing period type between consecutive ticks.
|
||||||
|
/// -> str
|
||||||
|
period: "month",
|
||||||
|
|
||||||
|
/// How to display years offsets.
|
||||||
|
/// -> str | function
|
||||||
|
year: "[year]",
|
||||||
|
|
||||||
|
/// How to display month offsets.
|
||||||
|
/// -> str | function
|
||||||
|
month: "[year]-[month repr:short]",
|
||||||
|
|
||||||
|
/// How to display day offsets.
|
||||||
|
/// -> str | function
|
||||||
|
day: "[year]-[month repr:short]-[day]"
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
#let datetime-smart-offset = e.element.declare(
|
||||||
|
"datetime-smart-offset",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => e.get(e-get =>
|
||||||
|
display-datetime-smart-offset(it, smart-first: e-get(datetime-smart-format).smart-first)
|
||||||
|
),
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("ticks", e.types.array(datetime), required: true),
|
||||||
|
e.field("avoid-redundant", bool, default: true),
|
||||||
|
e.field("period", e.types.option(str), default: "month"),
|
||||||
|
e.field("year", e.types.union(str, function), default: "[year]"),
|
||||||
|
e.field("month", e.types.union(str, function), default: "[year]-[month repr:short]"),
|
||||||
|
e.field("day", e.types.union(str, function), default: "[year]-[month repr:short]-[day]"),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Formats datetime ticks.
|
||||||
|
#let datetime(
|
||||||
|
|
||||||
|
/// The ticks to format.
|
||||||
|
/// -> array.
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// How to format the ticks. This can be a format string to be used with
|
||||||
|
/// #link(https://typst.app/docs/reference/foundations/datetime/#definitions-display)[`datetime.display`]
|
||||||
|
/// or a function that receives a datetime.
|
||||||
|
/// -> str | function
|
||||||
|
format: datetime-smart-format,
|
||||||
|
|
||||||
|
/// How to format the offset.
|
||||||
|
/// -> function
|
||||||
|
format-offset: datetime-smart-offset,
|
||||||
|
|
||||||
|
/// Additional information from the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-info: (:),
|
||||||
|
|
||||||
|
/// Arguments that are ignored by this formatter.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assert(
|
||||||
|
"mode" in tick-info,
|
||||||
|
message: "datetime can only be used with a datetime tick locator"
|
||||||
|
)
|
||||||
|
|
||||||
|
let period = tick-info.at("period", default: none)
|
||||||
|
let datetimes = time.to-datetime(
|
||||||
|
..ticks,
|
||||||
|
mode: if period == none { tick-info.mode } else { "datetime" }
|
||||||
|
)
|
||||||
|
|
||||||
|
// let offset-datetime = if min > max {
|
||||||
|
// datetimes.first()
|
||||||
|
// } else {
|
||||||
|
// datetimes.last()
|
||||||
|
// }
|
||||||
|
let offset = format-offset(datetimes, period: period)
|
||||||
|
|
||||||
|
let labels = if type(format) == function {
|
||||||
|
datetimes.map(dt => format(dt, period: period))
|
||||||
|
} else if type(format) == str {
|
||||||
|
datetimes.map(dt => dt.display(format))
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
labels: labels,
|
||||||
|
exponent: 0,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let set-datetime-smart-format = e.set_.with(datetime-smart-format)
|
||||||
|
#let set-datetime-smart-first = e.set_.with(datetime-smart-first)
|
||||||
|
#let set-datetime-smart-offset = e.set_.with(datetime-smart-offset)
|
||||||
1235
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/tick-locate.typ
vendored
Normal file
1235
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/tick-locate.typ
vendored
Normal file
File diff suppressed because it is too large
Load Diff
135
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/time.typ
vendored
Normal file
135
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/time.typ
vendored
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
#let reference-datetime = datetime(year: 0, month: 1, day: 1, hour: 0, minute: 0, second: 0)
|
||||||
|
#let reference-time = datetime(hour: 0, minute: 0, second: 0)
|
||||||
|
#let reference-date = datetime(year: 0, month: 1, day: 1)
|
||||||
|
|
||||||
|
#let to-seconds(..datetimes, return-mode: false) = {
|
||||||
|
datetimes = datetimes.pos()
|
||||||
|
if datetimes.len() == 0 { return () }
|
||||||
|
assert(
|
||||||
|
datetimes.all(dt => type(dt) == datetime),
|
||||||
|
message: "When passing datetime values, all entries need to be of type datetime"
|
||||||
|
)
|
||||||
|
|
||||||
|
let reference
|
||||||
|
let mode
|
||||||
|
let dt = datetimes.first()
|
||||||
|
let assert-same-type = assert.with(
|
||||||
|
message: "All datetimes must be of the same type (all time, all date, or all datetime)."
|
||||||
|
)
|
||||||
|
if dt.hour() == none {
|
||||||
|
mode = "date"
|
||||||
|
reference = reference-date
|
||||||
|
assert-same-type(datetimes.all(dt => dt.hour() == none))
|
||||||
|
} else if dt.year() == none {
|
||||||
|
mode = "time"
|
||||||
|
reference = reference-time
|
||||||
|
assert-same-type(datetimes.all(dt => dt.year() == none))
|
||||||
|
} else {
|
||||||
|
mode = "datetime"
|
||||||
|
reference = reference-datetime
|
||||||
|
assert-same-type(datetimes.all(dt => dt.hour() != none and dt.year() != none))
|
||||||
|
}
|
||||||
|
|
||||||
|
let seconds = datetimes.map(dt => (dt - reference).seconds())
|
||||||
|
if return-mode {
|
||||||
|
(mode: mode, seconds: seconds)
|
||||||
|
} else {
|
||||||
|
seconds
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let to-datetime(..seconds, mode: "datetime") = {
|
||||||
|
seconds = seconds.pos()
|
||||||
|
let reference = if mode == "datetime" {
|
||||||
|
reference-datetime
|
||||||
|
} else if mode == "date" {
|
||||||
|
reference-date
|
||||||
|
} else if mode == "time" {
|
||||||
|
reference-time
|
||||||
|
} else {
|
||||||
|
assert(false, "Unsupported mode")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
seconds.map(seconds => reference + duration(seconds: int(seconds)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#to-seconds(
|
||||||
|
datetime(year: 2025, month: 8, day: 4),
|
||||||
|
datetime(year: 2025, month: 8, day: 7),
|
||||||
|
// reference-datetime
|
||||||
|
)
|
||||||
|
#to-seconds(
|
||||||
|
// reference-time,
|
||||||
|
reference-datetime,
|
||||||
|
reference-datetime
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#let with(
|
||||||
|
date,
|
||||||
|
year: auto, month: auto, day: auto,
|
||||||
|
hour: auto, minute: auto, second: auto
|
||||||
|
) = {
|
||||||
|
if year == auto { year = date.year() }
|
||||||
|
if month == auto { month = date.month() }
|
||||||
|
if day == auto { day = date.day() }
|
||||||
|
|
||||||
|
if hour == auto { hour = date.hour() }
|
||||||
|
if minute == auto { minute = date.minute() }
|
||||||
|
if second == auto { second = date.second() }
|
||||||
|
|
||||||
|
let has-date = (year, month, day).any(x => x != none)
|
||||||
|
let has-time = (hour, minute, second).any(x => x != none)
|
||||||
|
if has-date and has-time {
|
||||||
|
datetime(
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day,
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
second: second
|
||||||
|
)
|
||||||
|
} else if has-date {
|
||||||
|
datetime(
|
||||||
|
year: year,
|
||||||
|
month: month,
|
||||||
|
day: day
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
datetime(
|
||||||
|
hour: hour,
|
||||||
|
minute: minute,
|
||||||
|
second: second
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(year: 2002, month: 3, day: 3), year: 1999),
|
||||||
|
datetime(year: 1999, month: 3, day: 3)
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(year: 2002, month: 3, day: 3), year: 1999, month: 12),
|
||||||
|
datetime(year: 1999, month: 12, day: 3)
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(year: 2002, month: 3, day: 3), day: 31, month: 12),
|
||||||
|
datetime(year: 2002, month: 12, day: 31)
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(hour: 2, minute: 22, second: 8), second: 7),
|
||||||
|
datetime(hour: 2, minute: 22, second: 7)
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(hour: 2, minute: 22, second: 8), minute: 27),
|
||||||
|
datetime(hour: 2, minute: 27, second: 8)
|
||||||
|
)
|
||||||
|
#assert.eq(
|
||||||
|
with(datetime(hour: 2, minute: 22, second: 8), hour: 7),
|
||||||
|
datetime(hour: 7, minute: 22, second: 8)
|
||||||
|
)
|
||||||
44
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/transform.typ
vendored
Normal file
44
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/logic/transform.typ
vendored
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
|
||||||
|
/// Creates a data-to-display transformation that maps the interval
|
||||||
|
/// $[x_0, x_1]$ to $[y_0, y_1]$ given some monotonous transformation function
|
||||||
|
/// (e.g., identity or log(x)).
|
||||||
|
///
|
||||||
|
/// -> function
|
||||||
|
#let create-trafo(
|
||||||
|
|
||||||
|
/// Transformation function that takes one argument.
|
||||||
|
/// -> function
|
||||||
|
transform,
|
||||||
|
|
||||||
|
/// Lower interval bound.
|
||||||
|
/// -> float
|
||||||
|
x0,
|
||||||
|
|
||||||
|
/// Upper interval bound.
|
||||||
|
/// -> float
|
||||||
|
x1,
|
||||||
|
|
||||||
|
/// Lower target interval bound.
|
||||||
|
/// -> float
|
||||||
|
y0: 0,
|
||||||
|
|
||||||
|
/// Upper target interval bound.
|
||||||
|
/// -> float
|
||||||
|
y1: 1
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let a = (y1 - y0) / (transform(x1) - transform(x0))
|
||||||
|
let b = -a * transform(x0) + y0
|
||||||
|
x => a * transform(x) + b
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(create-trafo(x => x, -23, 4)(-23), 0)
|
||||||
|
#assert.eq(create-trafo(x => x, -23, 4)(4), 1)
|
||||||
|
|
||||||
|
#assert.eq(create-trafo(x => x, -23, 4, y0: 1, y1: -1)(-23), 1)
|
||||||
|
#assert.eq(create-trafo(x => x, -23, 4, y0: 1, y1: -1)(4), -1)
|
||||||
|
|
||||||
|
#assert.eq(create-trafo(x => calc.log(x), 2, 4)(2), 0)
|
||||||
|
#assert.eq(create-trafo(x => calc.log(x), 2, 4)(4), 1)
|
||||||
|
|
||||||
|
|
||||||
332
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/math.typ
vendored
Normal file
332
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/math.typ
vendored
Normal file
@@ -0,0 +1,332 @@
|
|||||||
|
|
||||||
|
#import "vec.typ"
|
||||||
|
#import "algorithm/interpolate.typ"
|
||||||
|
#import "loading/txt.typ": load-txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the sign of a number.
|
||||||
|
/// - `0` for $x=0$
|
||||||
|
/// - `1` for $x>0$
|
||||||
|
/// - `-1` for $x<0$
|
||||||
|
/// Note that this is different from the built-in
|
||||||
|
/// [`float.signum()`](https://typst.app/docs/reference/foundations/float/#definitions-signum)
|
||||||
|
/// which returns `1.0` for $x=0$.
|
||||||
|
/// -> int
|
||||||
|
#let sign(
|
||||||
|
/// The value to determine the sign of.
|
||||||
|
/// -> int | float
|
||||||
|
value
|
||||||
|
) = if value == 0 { 0 } else if value < 0 { -1 } else { 1 }
|
||||||
|
|
||||||
|
|
||||||
|
/// Returns the minimum value of an array, ignoring `NaN` values. Returns `none`
|
||||||
|
/// if the array is empty or contains only `NaN` values.
|
||||||
|
/// -> none | float
|
||||||
|
#let cmin(
|
||||||
|
/// Values to compute the minimum of.
|
||||||
|
/// -> array
|
||||||
|
values
|
||||||
|
) = {
|
||||||
|
values = values.filter(x => not float.is-nan(x))
|
||||||
|
if values.len() == 0 { return none }
|
||||||
|
return calc.min(..values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the maximum value of an array, ignoring `NaN` values. Returns `none`
|
||||||
|
/// if the array is empty or contains only `NaN` values.
|
||||||
|
/// -> none | float
|
||||||
|
#let cmax(
|
||||||
|
/// Values to compute the maximum of.
|
||||||
|
/// -> array
|
||||||
|
values
|
||||||
|
) = {
|
||||||
|
values = values.filter(x => not float.is-nan(x))
|
||||||
|
if values.len() == 0 { return none }
|
||||||
|
return calc.max(..values)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the minimum and maximum value of an array, ignoring `NaN` values.
|
||||||
|
/// Returns `(none, none)` if the array is empty or contains only `NaN` values.
|
||||||
|
/// -> array
|
||||||
|
#let minmax(
|
||||||
|
/// Values to compute the minimum and the maximum of.
|
||||||
|
/// -> array
|
||||||
|
values
|
||||||
|
) = {
|
||||||
|
values = values.filter(x => not float.is-nan(x))
|
||||||
|
if values.len() == 0 { return (none, none) }
|
||||||
|
return (calc.min(..values), calc.max(..values))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(cmin((1,2,3,4)), 1)
|
||||||
|
#assert.eq(cmin((1,2,3,-23.2)), -23.2)
|
||||||
|
#assert.eq(cmin((float.nan,2,3,-23.2)), -23.2)
|
||||||
|
#assert.eq(cmin((float.nan,)), none)
|
||||||
|
#assert.eq(cmax((1,2,3,4)), 4)
|
||||||
|
#assert.eq(cmax((-1,-2,-3,-23.2)), -1)
|
||||||
|
#assert.eq(cmax((float.nan,2,3,-23.2)), 3)
|
||||||
|
#assert.eq(cmax((float.nan,)), none)
|
||||||
|
|
||||||
|
#assert.eq(float.is-nan(float.nan), true)
|
||||||
|
#assert.eq(float.is-nan(1e123), false)
|
||||||
|
#assert.eq(float.is-nan(0), false)
|
||||||
|
#assert.eq(float.is-nan(-1232445345345e200000000000000000), false)
|
||||||
|
|
||||||
|
|
||||||
|
/// Generates an array of evenly-spaced numbers in the interval `[start, end)` or `[start, end]`.
|
||||||
|
/// -> array
|
||||||
|
#let linspace(
|
||||||
|
|
||||||
|
/// Start of the range.
|
||||||
|
/// -> int | float
|
||||||
|
start,
|
||||||
|
|
||||||
|
/// End of the range.
|
||||||
|
/// -> int | float
|
||||||
|
end,
|
||||||
|
|
||||||
|
/// Number of evenly-spaced values to produce.
|
||||||
|
/// -> int
|
||||||
|
num: 50,
|
||||||
|
|
||||||
|
/// Whether to include the end of the range. If `true`, samples are taken for
|
||||||
|
/// the closed interval `[start, end]`.
|
||||||
|
/// -> bool
|
||||||
|
include-end: true
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assert(num >= 0, message: "linspace: num must be non-negative")
|
||||||
|
if num == 0 { return () }
|
||||||
|
if num == 1 { return (start,) }
|
||||||
|
let k = (end - start) / (num - int(include-end))
|
||||||
|
range(num).map(x => k * x + start)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Special cases: num = 0,1
|
||||||
|
#assert.eq(linspace(0, 1, num: 0), ())
|
||||||
|
#assert.eq(linspace(0, 1, num: 0, include-end: false), ())
|
||||||
|
#assert.eq(linspace(-2.3, 1, num: 1), (-2.3, ))
|
||||||
|
#assert.eq(linspace(-2.3, 1, num: 1, include-end: false), (-2.3, ))
|
||||||
|
|
||||||
|
// Normal operation
|
||||||
|
#assert.eq(linspace(0, 1, num: 2), (0, 1))
|
||||||
|
#assert.eq(linspace(0, 1, num: 2, include-end: false), (0, .5))
|
||||||
|
#assert.eq(linspace(-3.4, 7, num: 2), (-3.4, 7))
|
||||||
|
#assert.eq(linspace(-3, 7, num: 2, include-end: false), (-3, 2))
|
||||||
|
#assert.eq(linspace(0, 1, num: 5), (0, .25, .5, .75, 1))
|
||||||
|
|
||||||
|
// Inverse range
|
||||||
|
#assert.eq(linspace(1, 0, num: 2), (1, 0))
|
||||||
|
#assert.eq(linspace(100, 0, num: 2), (100, 0))
|
||||||
|
#assert.eq(linspace(100, 0, num: 2, include-end: false), (100, 50))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Generates an array of numbers spaced by `step` in the interval `[start, end)`.
|
||||||
|
/// -> array
|
||||||
|
#let arange(
|
||||||
|
|
||||||
|
/// Start of the range.
|
||||||
|
/// -> int | float
|
||||||
|
start,
|
||||||
|
|
||||||
|
/// End of the range (excluded).
|
||||||
|
/// -> int | float
|
||||||
|
end,
|
||||||
|
|
||||||
|
/// Difference between consecutive values.
|
||||||
|
/// -> int | float
|
||||||
|
step: 1
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let num = calc.quo(end - start, step)
|
||||||
|
range(num).map(x => x * step + start)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let arange1(
|
||||||
|
start,
|
||||||
|
end,
|
||||||
|
step: 1,
|
||||||
|
) = {
|
||||||
|
let num = calc.quo(calc.abs(end - start), step)
|
||||||
|
range(num).map(x => x * step + start)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(arange(0, 1), (0,))
|
||||||
|
#assert.eq(arange(0, 2), (0,1))
|
||||||
|
#assert.eq(arange(0, 1, step: 0.25), (0,.25, .5, .75))
|
||||||
|
#assert.eq(arange(0, 2, step: 0.25), (0,.25, .5, .75, 1, 1.25, 1.5, 1.75))
|
||||||
|
|
||||||
|
// Inverse range
|
||||||
|
#assert.eq(arange(1, 0), ())
|
||||||
|
#assert.eq(arange(41, 0), ())
|
||||||
|
#assert.eq(arange(1, 0, step: -1), (1,))
|
||||||
|
#assert.eq(arange(0, -4, step: -1), (0, -1, -2, -3))
|
||||||
|
#assert.eq(arange(1, 0, step: -0.25), (1,.75, .5, .25))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Computes the q-th percentile of the given data.
|
||||||
|
#let percentile(
|
||||||
|
|
||||||
|
/// Array of values.
|
||||||
|
/// -> array
|
||||||
|
values,
|
||||||
|
|
||||||
|
/// A percentage between 0% and 100%.
|
||||||
|
/// -> ratio
|
||||||
|
q,
|
||||||
|
|
||||||
|
/// Interpolation method. Currently, only `"linear"` is supported.
|
||||||
|
/// -> "linear"
|
||||||
|
method: "linear"
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assert(method in ("linear",), message: "`percentile`: unknown method \"" + method + "\"")
|
||||||
|
|
||||||
|
|
||||||
|
return interpolate.linear(values, q / 100% * (values.len() - 1))
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(percentile((1,2,3), 50%), 2)
|
||||||
|
#assert.eq(percentile((1,2,3), 25%), 1.5)
|
||||||
|
#assert.eq(percentile((1,2,3), 0%), 1)
|
||||||
|
#assert.eq(percentile((1,2,3), 100%), 3)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a rectangular mesh from two input arrays.
|
||||||
|
/// One or more functions are evaluated for each possible pair $(x_i,y_j)$ of
|
||||||
|
/// the inputs $\{x_0,...\}$ and $\{y_0,...\}$.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.mesh((0, 1), (4, 5), (x, y) => (x + y))
|
||||||
|
/// ```
|
||||||
|
/// Returns the array `(x, y, ..zs)` where `zs` are two-dimensional arrays
|
||||||
|
/// ```
|
||||||
|
/// (
|
||||||
|
/// (f(x0, y0), f(x0, y1), ...),
|
||||||
|
/// (f(x1, y0), f(x1, y1), ...),
|
||||||
|
/// ...
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// for each function that was passed to `mesh()`.
|
||||||
|
/// -> array
|
||||||
|
#let mesh(
|
||||||
|
|
||||||
|
/// Array of $x$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Array of $y$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Bivariate functions that take two numbers `x, y` as inputs.
|
||||||
|
/// -> function
|
||||||
|
..transforms
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let transforms = transforms.pos()
|
||||||
|
let mesh = transforms.map(transform => y.map(y => x.map(x => transform(x, y))))
|
||||||
|
if transforms.len() == 1 {
|
||||||
|
mesh = mesh.first()
|
||||||
|
}
|
||||||
|
return mesh
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
mesh((0, 2), (4, 5), (x, y) => (x + y/10)),
|
||||||
|
((0.4, 2.4), (0.5, 2.5))
|
||||||
|
)
|
||||||
|
|
||||||
|
#assert.eq(
|
||||||
|
mesh((0, 2), (4, 5), (x, y) => (x + y/10), (x, y) => (x - y)),
|
||||||
|
(
|
||||||
|
((0.4, 2.4), (0.5, 2.5)),
|
||||||
|
((-4, -2), (-5, -3)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Performs integer (floored) division and returns `(quotient, remainder)` while
|
||||||
|
/// guaranteeing that `quotient * divisor + remainder = dividend`. Note that using
|
||||||
|
/// `calc.quo` and `calc.rem` does not give this guarantee.
|
||||||
|
/// -> array
|
||||||
|
#let divmod(
|
||||||
|
/// The dividend of the quotient.
|
||||||
|
/// -> int | float
|
||||||
|
dividend,
|
||||||
|
|
||||||
|
/// The divisor of the quotient.
|
||||||
|
/// -> int | float
|
||||||
|
divisor
|
||||||
|
) = {
|
||||||
|
let q = calc.quo(dividend, divisor)
|
||||||
|
return (q, dividend - q * divisor)
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(divmod(5, 2), (2, 1))
|
||||||
|
#assert.eq(divmod(5, .5), (10, 0))
|
||||||
|
#assert.eq(divmod(5.25, .5), (10, 0.25))
|
||||||
|
#assert.eq(divmod(5.25, -.5), (-11, -0.25))
|
||||||
|
#assert.eq(divmod(5.25, -.5), (-11, -0.25))
|
||||||
|
#assert.eq(divmod(-5.25, .5), (-11, 0.25))
|
||||||
|
#assert.eq(divmod(-5.25, -.5), (10, -0.25))
|
||||||
|
#assert.eq(divmod(2, 1), (2, 0))
|
||||||
|
#assert.eq(divmod(-2, 1), (-2, 0))
|
||||||
|
#assert.eq(divmod(1, .2), (5, 0))
|
||||||
|
#assert.eq(divmod(5, .2), (25, 0))
|
||||||
|
|
||||||
|
|
||||||
|
#{
|
||||||
|
let check(x, d) = {
|
||||||
|
let quo = calc.div-euclid(x, d)
|
||||||
|
let rem = calc.rem(x, d)
|
||||||
|
let (quo, rem) = divmod(x, d)
|
||||||
|
assert.eq(quo*d + rem, x)
|
||||||
|
}
|
||||||
|
check(5, -.2)
|
||||||
|
check(2, 1)
|
||||||
|
check(-2, 1)
|
||||||
|
check(1, .2)
|
||||||
|
check(-5, .2)
|
||||||
|
check(-5, -.2)
|
||||||
|
check(115, 22)
|
||||||
|
check(115, .22)
|
||||||
|
check(1e30, 1e20)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Decomposes a floating point number into the scientific notation.
|
||||||
|
/// $ x = a\cdot 10^n $
|
||||||
|
/// where $a \in [0.1, 1)$ and $n \in \mathbb{Z}$. Returns an array `(a, n)`.
|
||||||
|
/// -> array
|
||||||
|
#let decompose-floating-point(
|
||||||
|
/// Number to decompose.
|
||||||
|
/// -> float
|
||||||
|
value
|
||||||
|
) = {
|
||||||
|
let n = int(calc.floor(calc.log(base: 10, value))) + 1
|
||||||
|
let a = value / calc.pow(10., n)
|
||||||
|
return (a, n)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Computes $10^x$ for the given number $x$, guaranteeing floating point
|
||||||
|
/// computation, even when the input is an `int`.
|
||||||
|
/// -> float
|
||||||
|
#let pow10(
|
||||||
|
/// The exponent to which to raise the number 10.
|
||||||
|
/// -> int | float
|
||||||
|
value
|
||||||
|
) = calc.pow(10., value)
|
||||||
933
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/axis.typ
vendored
Normal file
933
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/axis.typ
vendored
Normal file
@@ -0,0 +1,933 @@
|
|||||||
|
#import "../logic/scale.typ" as lqscale
|
||||||
|
#import "../utility.typ": place-in-out, match, match-type, if-auto, if-none
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../logic/tick-locate.typ"
|
||||||
|
#import "../logic/tick-format.typ"
|
||||||
|
#import "../bounds.typ": *
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../model/label.typ": xlabel, ylabel, label as lq-label
|
||||||
|
#import "../process-styles.typ": update-stroke, merge-strokes
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
#import "tick.typ": tick as lq-tick, tick-label as lq-tick-label
|
||||||
|
#import "spine.typ": spine
|
||||||
|
|
||||||
|
#import "@preview/zero:0.5.0"
|
||||||
|
#import "@preview/tiptoe:0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// An axis for a diagram. Visually, an axis consists of a _spine_ along the axis
|
||||||
|
/// direction, a collection of ticks/subticks and an axis label.
|
||||||
|
///
|
||||||
|
/// By default, a @diagram features two axes: an `x` and a `y` axis which can be
|
||||||
|
/// configured directly through @diagram.xaxis and @diagram.yaxis. However, it is
|
||||||
|
/// also possible to add more axes, please refer to the
|
||||||
|
/// #link("tutorials/axis")[axis tutorial] for more details.
|
||||||
|
///
|
||||||
|
/// The built-in tick formatters use the Typst package
|
||||||
|
/// #link("https://typst.app/universe/package/zero")[Zero] for displaying
|
||||||
|
/// numbers. This makes it possible to define a consistent number format
|
||||||
|
/// throughout the entire document, including tables, in-text quantities,
|
||||||
|
/// and figures.
|
||||||
|
#let axis(
|
||||||
|
|
||||||
|
/// Sets the scale of the axis. This may be a @scale object or the name of
|
||||||
|
/// one of the built-in scales `"linear"`, `"log"`, `"symlog"`, and
|
||||||
|
/// `"datetime"`.
|
||||||
|
///
|
||||||
|
/// If left at `auto`, the scale will be set to `"datetime"` if any of the
|
||||||
|
/// plots uses datetime coordinates and `"linear"` otherwise.
|
||||||
|
/// -> auto | str | lq.scale
|
||||||
|
scale: auto,
|
||||||
|
|
||||||
|
/// Data limits of the axis. This can be used to fix the minimum and/or maximum value
|
||||||
|
/// displayed along this axis. This parameter expects `auto` or a tuple `(min, max)`
|
||||||
|
/// where `min` and `max` can also be `auto`. If a limit is `auto`, it will be
|
||||||
|
/// automatically computed from all plots associated with this axis and @diagram.margin
|
||||||
|
/// will be applied. If the minimum is larger than the maximum, the scale is inverted
|
||||||
|
/// and if `min` and `max` coincide, the range will be automatically increased.
|
||||||
|
/// Also see @axis.inverted.
|
||||||
|
/// -> auto | array
|
||||||
|
lim: auto,
|
||||||
|
|
||||||
|
/// Whether to invert the limits (swap minimum and maximum). Inverting is
|
||||||
|
/// applied regardless of whether the limits are set manually or computed automatically.
|
||||||
|
/// -> bool
|
||||||
|
inverted: false,
|
||||||
|
|
||||||
|
/// Label for the axis. Use a @label object for more options.
|
||||||
|
/// -> content | lq.label
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// The kind of the axis.
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
/// Where to place this axis. This can be
|
||||||
|
/// - one of the sides of the diagram (`top` or `bottom` for $x$-axes,
|
||||||
|
/// `left` or `right` for $y$-axes),
|
||||||
|
/// - a `float` coordinate value on the other axis,
|
||||||
|
/// - a `length` or `relative`,
|
||||||
|
/// - or a combination of the first and third option through a dictionary
|
||||||
|
/// with the keys `align` and `offset`.
|
||||||
|
///
|
||||||
|
/// More on axis placement can be found in the
|
||||||
|
/// #link("tutorials/axis#placement-and-mirrors")[axis tutorial].
|
||||||
|
/// -> auto | alignment | float | relative | dictionary
|
||||||
|
position: auto,
|
||||||
|
|
||||||
|
/// Whether to mirror the axis, i.e., whether to show the axis ticks also on
|
||||||
|
/// the side opposite of the one specified with @axis.position. When set to
|
||||||
|
/// `auto`, mirroring is only activated when `position: auto`. More control
|
||||||
|
/// is granted through a dictionary with the possible keys `ticks` and
|
||||||
|
/// `tick-labels` to individually activate or deactivate those.
|
||||||
|
///
|
||||||
|
/// More on axis mirrors can be found in the
|
||||||
|
/// #link("tutorials/axis#placement-and-mirrors")[axis tutorial].
|
||||||
|
/// -> auto | bool | dictionary
|
||||||
|
mirror: auto,
|
||||||
|
|
||||||
|
/// Instead of using the tick locator, specifies the tick locations explicitly
|
||||||
|
/// and optionally the tick labels. This can be an array with just the tick
|
||||||
|
/// location or tuples of tick location and label, or a dictionary with the
|
||||||
|
/// keys `ticks` and `labels`, containing arrays of equal length. When `ticks`
|
||||||
|
/// is `none`, no ticks are displayed. If it is `auto`, the `tick-locator` is
|
||||||
|
/// used.
|
||||||
|
///
|
||||||
|
/// Check out the #link("tutorials/ticks")[tutorial on ticks] for tips on how
|
||||||
|
/// to work with ticks.
|
||||||
|
/// -> auto | array | dictionary | none
|
||||||
|
ticks: auto,
|
||||||
|
|
||||||
|
/// Instead of using the tick locator, specifies the tick positions explicitly
|
||||||
|
/// and optionally the tick labels.
|
||||||
|
///
|
||||||
|
/// Also see the #link("tutorials/ticks")[tutorial on ticks].
|
||||||
|
/// -> auto | none | int
|
||||||
|
subticks: auto,
|
||||||
|
|
||||||
|
/// Passes the parameter `tick-distance` to the tick locator. The linear tick
|
||||||
|
/// locator respects this setting and sets the distance between consecutive
|
||||||
|
/// ticks accordingly. If `tick-args` already contains an entry `tick-distance`,
|
||||||
|
/// it takes precedence.
|
||||||
|
/// -> auto | float
|
||||||
|
tick-distance: auto,
|
||||||
|
|
||||||
|
/// Offset for all ticks on this axis. The offset is subtracted from all ticks
|
||||||
|
/// and shown at the end of the axis (if it is not 0). An offset can be used
|
||||||
|
/// to avoid overly long tick labels and to focus on the relative distance
|
||||||
|
/// between data points.
|
||||||
|
///
|
||||||
|
/// If `none` or a value of type `content`, the offset is just displayed and
|
||||||
|
/// has no effect on how the data is presented.
|
||||||
|
/// -> auto | int | float | content | none
|
||||||
|
offset: auto,
|
||||||
|
|
||||||
|
/// Exponent for all ticks on this axis. All ticks are divided by
|
||||||
|
/// $10^\mathrm{exponent}$ and the $10^\mathrm{exponent}$ is shown at the end
|
||||||
|
/// of the axis (if the exponent is not 0). This setting can be used to avoid
|
||||||
|
/// overly long tick labels.
|
||||||
|
///
|
||||||
|
/// In combination with logarithmic tick locators, `none` can be used to
|
||||||
|
/// force writing out all numbers.
|
||||||
|
/// -> auto | none | int | "inline"
|
||||||
|
exponent: auto,
|
||||||
|
|
||||||
|
/// Threshold for automatic exponents.
|
||||||
|
/// -> int
|
||||||
|
auto-exponent-threshold: 3,
|
||||||
|
|
||||||
|
/// The tick locator for the regular ticks.
|
||||||
|
/// Also see #link("tutorials/ticks#locating-ticks")[locating ticks].
|
||||||
|
/// -> auto | function
|
||||||
|
locate-ticks: auto,
|
||||||
|
|
||||||
|
/// The formatter for the (major) ticks.
|
||||||
|
/// Also see #link("tutorials/ticks#formatting-ticks")[formatting ticks].
|
||||||
|
/// -> auto | function
|
||||||
|
format-ticks: auto,
|
||||||
|
|
||||||
|
/// The tick locator for the subticks.
|
||||||
|
/// Also see #link("tutorials/ticks#locating-ticks")[locating ticks].
|
||||||
|
/// -> auto | function
|
||||||
|
locate-subticks: auto,
|
||||||
|
|
||||||
|
/// The formatter for the subticks.
|
||||||
|
/// Also see #link("tutorials/ticks#displaying-subtick-labels")[displaying subticks].
|
||||||
|
/// -> auto | none | function
|
||||||
|
format-subticks: none,
|
||||||
|
|
||||||
|
/// An array of extra ticks to display. The ticks can be positions given as `float` data values or @tick instances.
|
||||||
|
/// -> array
|
||||||
|
extra-ticks: (),
|
||||||
|
|
||||||
|
/// The formatter for the extra ticks.
|
||||||
|
format-extra-ticks: none,
|
||||||
|
|
||||||
|
/// Arguments to pass to the tick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
tick-args: (:),
|
||||||
|
|
||||||
|
/// Arguments to pass to the subtick locator.
|
||||||
|
/// -> dictionary
|
||||||
|
subtick-args: (:),
|
||||||
|
|
||||||
|
/// Specifies conversions between the data and the ticks. This can be used to
|
||||||
|
/// configure a secondary axis to display the same data in a different unit,
|
||||||
|
/// e.g., the main axis displays the velocity of a particle while the
|
||||||
|
/// secondary axis displays the associated energy. In this case, one would
|
||||||
|
/// pick `functions: (x => m*x*x, y => calc.sqrt(y/m))` with some constant
|
||||||
|
/// `m`. Note that the first function computes the "forward" direction while
|
||||||
|
/// the second function computes the "backward" direction. The user needs to
|
||||||
|
/// ensure that the two functions are really inverses of each other.
|
||||||
|
/// By default, this parameter resolves to the identity.
|
||||||
|
/// -> auto | array
|
||||||
|
functions: auto,
|
||||||
|
|
||||||
|
/// If set to `true`, the entire axis is hidden.
|
||||||
|
/// -> bool
|
||||||
|
hidden: false,
|
||||||
|
|
||||||
|
/// How to stroke the spine of the axis. If not `auto`, this is forwarded to
|
||||||
|
/// @spine.stroke.
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// Places an arrow tip on the axis spine. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// If not `auto`, this is forwarded to @spine.tip.
|
||||||
|
/// -> auto | none | tiptoe.mark
|
||||||
|
tip: auto,
|
||||||
|
|
||||||
|
/// Places an arrow tail on the axis spine. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// If not `auto`, this is forwarded to @spine.toe.
|
||||||
|
/// -> auto | none | tiptoe.mark
|
||||||
|
toe: auto,
|
||||||
|
|
||||||
|
filter: (value, distance) => true,
|
||||||
|
|
||||||
|
/// Plot objects to associate with this axis. This only applies when this is
|
||||||
|
/// a secondary axis. Automatic limits are then computed according to this
|
||||||
|
/// axis and transformations of the data coordinates linked to the scaling of
|
||||||
|
/// this axis.
|
||||||
|
/// -> any
|
||||||
|
..plots
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(plots)
|
||||||
|
plots = plots.pos()
|
||||||
|
if "tick-distance" not in tick-args {
|
||||||
|
tick-args.tick-distance = tick-distance
|
||||||
|
}
|
||||||
|
|
||||||
|
if scale == auto {
|
||||||
|
scale = "linear"
|
||||||
|
|
||||||
|
for plot in plots {
|
||||||
|
if "axis-id" in plot { plot = plot.plot }
|
||||||
|
if "datetime" in plot and plot.datetime.at(kind, default: false) {
|
||||||
|
scale = "datetime"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(scale) == str {
|
||||||
|
assert(scale in lqscale.scales, message: "Unknown scale " + scale)
|
||||||
|
scale = lqscale.scales.at(scale)
|
||||||
|
}
|
||||||
|
assert(kind in ("x", "y"), message: "The `kind` of an axis can only be `x` or `y`")
|
||||||
|
let orthogonal-offset = 0pt
|
||||||
|
|
||||||
|
|
||||||
|
if position == auto {
|
||||||
|
if kind == "x" { position = bottom }
|
||||||
|
else if kind == "y" { position = left }
|
||||||
|
} else if type(position) in (int, float, length, relative, ratio) {
|
||||||
|
if kind == "x" {
|
||||||
|
orthogonal-offset = position
|
||||||
|
position = bottom
|
||||||
|
}
|
||||||
|
else if kind == "y" {
|
||||||
|
orthogonal-offset = position
|
||||||
|
position = left
|
||||||
|
}
|
||||||
|
if mirror == auto { mirror = none }
|
||||||
|
} else if type(position) == dictionary {
|
||||||
|
assertations.assert-dict-keys(position, mandatory: ("align", "offset"))
|
||||||
|
|
||||||
|
|
||||||
|
if kind == "x" { orthogonal-offset = position.offset }
|
||||||
|
else if kind == "y" { orthogonal-offset = position.offset }
|
||||||
|
position = position.align
|
||||||
|
if mirror == auto { mirror = none }
|
||||||
|
|
||||||
|
if kind == "x" {
|
||||||
|
assert(position in (top, bottom), message: "For x-axes, `position` can only be \"top\" or \"bottom\", got " + repr(position))
|
||||||
|
}
|
||||||
|
if kind == "y" {
|
||||||
|
assert(position in (left, right), message: "For y-axes, `position` can only be \"left\" or \"right\", got " + repr(position))
|
||||||
|
}
|
||||||
|
} else if type(position) == alignment {
|
||||||
|
if mirror == auto { mirror = none }
|
||||||
|
|
||||||
|
if kind == "x" {
|
||||||
|
assert(position in (top, bottom), message: "For x-axes, `position` can only be \"top\" or \"bottom\", got " + repr(position))
|
||||||
|
}
|
||||||
|
if kind == "y" {
|
||||||
|
assert(position in (left, right), message: "For y-axes, `position` can only be \"left\" or \"right\", got " + repr(position))
|
||||||
|
}
|
||||||
|
} else {}
|
||||||
|
|
||||||
|
if ticks != auto {
|
||||||
|
if type(ticks) == dictionary {
|
||||||
|
assert("ticks" in ticks, message: "When passing a dictionary for `ticks`, you need to provide the keys \"ticks\" and optionally \"labels\"")
|
||||||
|
locate-ticks = tick-locate.manual.with(ticks: ticks.ticks.zip(ticks.labels))
|
||||||
|
if "labels" in ticks {
|
||||||
|
format-ticks = tick-locate.format-ticks-manual
|
||||||
|
}
|
||||||
|
} else if type(ticks) == array {
|
||||||
|
if ticks.len() > 0 and type(ticks.first()) == array {
|
||||||
|
// let (ticks, labels) = array.zip(..ticks)
|
||||||
|
locate-ticks = tick-locate.manual.with(ticks: ticks)
|
||||||
|
format-ticks = tick-format.manual
|
||||||
|
} else {
|
||||||
|
locate-ticks = tick-locate.manual.with(ticks: ticks)
|
||||||
|
}
|
||||||
|
} else if ticks == none {
|
||||||
|
locate-ticks = none
|
||||||
|
format-ticks = none
|
||||||
|
} else { assert(false, message: "The parameter `ticks` may either be an array or a dictionary")}
|
||||||
|
}
|
||||||
|
|
||||||
|
if locate-ticks == auto {
|
||||||
|
locate-ticks = if-none(scale.locate-ticks, tick-locate.linear)
|
||||||
|
}
|
||||||
|
if format-ticks == auto {
|
||||||
|
format-ticks = match(
|
||||||
|
scale.name,
|
||||||
|
"linear", () => tick-format.linear,
|
||||||
|
"log", () => tick-format.log.with(base: scale.base),
|
||||||
|
"symlog", () => tick-format.symlog.with(base: scale.base, threshold: scale.threshold, linscale: scale.linscale),
|
||||||
|
"datetime", () => tick-format.datetime,
|
||||||
|
default: () => tick-format.naive
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if subticks == none {
|
||||||
|
locate-subticks = none
|
||||||
|
} else if type(subticks) == int {
|
||||||
|
locate-subticks = tick-locate.subticks-linear.with(num: subticks)
|
||||||
|
} else if subticks != auto {
|
||||||
|
assert(false, message: "Unsupported argument type `" + str(type(subticks)) + "` for parameter `subticks`")
|
||||||
|
}
|
||||||
|
if locate-subticks == auto {
|
||||||
|
locate-subticks = if-none(scale.locate-subticks, none)
|
||||||
|
}
|
||||||
|
let is-independant = plots.len() > 0
|
||||||
|
if functions == auto { functions = (x => x, x => x) }
|
||||||
|
else {
|
||||||
|
assert(type(functions) == array and functions.map(type) == (function, function), message: "The parameter `functions` for `axis()` expects an array of two functions, a forward and an inverse function.")
|
||||||
|
assert(plots.len() == 0, message: "An `axis` can either be created with `functions` or with `..plots` but not both. ")
|
||||||
|
assert(lim == auto, message: "A dependent `axis` with `functions` is not allowed to have manual axis limits. ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(lim) == array {
|
||||||
|
assert.eq(lim.len(), 2, message: "Limit arrays must contain exactly two items")
|
||||||
|
lim = lim.map(
|
||||||
|
lim => if type(lim) == datetime {
|
||||||
|
time.to-seconds(lim).first()
|
||||||
|
} else {
|
||||||
|
lim
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
} else if lim == auto {
|
||||||
|
lim = (auto, auto)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "Unsupported limit specification")
|
||||||
|
}
|
||||||
|
|
||||||
|
if mirror == auto {
|
||||||
|
mirror = (ticks: true)
|
||||||
|
} else if type(mirror) == bool {
|
||||||
|
if mirror { mirror = (ticks: true) }
|
||||||
|
else { mirror = none }
|
||||||
|
} else if type(mirror) == dictionary {
|
||||||
|
for key in mirror.keys() {
|
||||||
|
assert(key in ("ticks", "tick-labels"), message: "When passing a dictionary to `axis.mirror`, only the keys \"ticks\" and \"tick-labels\" are valid, got \"" + key + "\"")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
type: "axis",
|
||||||
|
scale: scale,
|
||||||
|
lim: lim,
|
||||||
|
functions: (forward: functions.at(0), inv: functions.at(1)),
|
||||||
|
label: label,
|
||||||
|
stroke: stroke,
|
||||||
|
kind: kind,
|
||||||
|
position: position,
|
||||||
|
orthogonal-offset: orthogonal-offset,
|
||||||
|
mirror: mirror,
|
||||||
|
|
||||||
|
locate-ticks: locate-ticks,
|
||||||
|
format-ticks: format-ticks,
|
||||||
|
locate-subticks: locate-subticks,
|
||||||
|
format-subticks: format-subticks,
|
||||||
|
extra-ticks: extra-ticks,
|
||||||
|
format-extra-ticks: format-extra-ticks,
|
||||||
|
|
||||||
|
filter: filter,
|
||||||
|
|
||||||
|
tick-args: tick-args,
|
||||||
|
subtick-args: subtick-args,
|
||||||
|
|
||||||
|
offset: offset,
|
||||||
|
exponent: exponent,
|
||||||
|
auto-exponent-threshold: auto-exponent-threshold,
|
||||||
|
plots: plots,
|
||||||
|
hidden: hidden,
|
||||||
|
tip: tip,
|
||||||
|
toe: toe,
|
||||||
|
|
||||||
|
inverted: inverted
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#let xaxis = axis.with(kind: "x")
|
||||||
|
#let yaxis = axis.with(kind: "y")
|
||||||
|
|
||||||
|
|
||||||
|
/// Computes the axis limits of one axis (x or y). For each plot, `xlimits()`
|
||||||
|
/// and `ylimits()` is called. Plots whose limit functions return `none`
|
||||||
|
/// will be ignored.
|
||||||
|
/// If there are no plots or all calls to `xlimits()` and
|
||||||
|
/// `ylimits()` return `none`, the limits are set to `(0,0)`.
|
||||||
|
///
|
||||||
|
/// Regardless of this, a zero-width range is enlarged by calling
|
||||||
|
/// `next(min, -1)` and `next(max, 1)`.
|
||||||
|
/// Finally lower and upper margins are applied.
|
||||||
|
///
|
||||||
|
#let _axis-compute-limits(
|
||||||
|
axis,
|
||||||
|
lower-margin: 0%, upper-margin: 0%,
|
||||||
|
default-lim: (0, 1),
|
||||||
|
is-independant: auto
|
||||||
|
) = {
|
||||||
|
if is-independant == auto {
|
||||||
|
is-independant = axis.plots.len() > 0
|
||||||
|
}
|
||||||
|
let axis-type = match(axis.kind, "x", "x", "y", "y")
|
||||||
|
let (x0, x1) = (none, none)
|
||||||
|
let (tight0, tight1) = (true, true)
|
||||||
|
|
||||||
|
|
||||||
|
if axis.lim.at(0) != auto { x0 = axis.lim.at(0); tight0 = true }
|
||||||
|
if axis.lim.at(1) != auto { x1 = axis.lim.at(1); tight1 = true }
|
||||||
|
|
||||||
|
if auto in axis.lim {
|
||||||
|
if is-independant {
|
||||||
|
let plot-limits = axis.plots.map(plot => plot.at(axis-type + "limits")())
|
||||||
|
.filter(x => x != none)
|
||||||
|
if plot-limits.len() == 0 {
|
||||||
|
(x0, x1) = (0, 1)
|
||||||
|
if axis.scale.identity != 0 {
|
||||||
|
(x0, x1) = (axis.scale.identity,) * 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (plot-x0, plot-x1) in plot-limits {
|
||||||
|
let tight-bound = (false, false)
|
||||||
|
if type(plot-x0) == fraction { plot-x0 /= 1fr; tight-bound.at(0) = true }
|
||||||
|
if axis.lim.at(0) == auto and plot-x0 != none and (x0 == none or plot-x0 < x0) {
|
||||||
|
x0 = plot-x0
|
||||||
|
tight0 = tight-bound.at(0)
|
||||||
|
}
|
||||||
|
if type(plot-x1) == fraction { plot-x1 /= 1fr; tight-bound.at(1) = true }
|
||||||
|
if axis.lim.at(1) == auto and plot-x0 != none and (x1 == none or plot-x1 > x1) {
|
||||||
|
x1 = plot-x1
|
||||||
|
tight1 = tight-bound.at(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(x0, x1) = default-lim.map(axis.functions.forward)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if x0 == x1 {
|
||||||
|
x0 = (axis.scale.inverse)((axis.scale.transform)(x0) - 1)
|
||||||
|
x1 = (axis.scale.inverse)((axis.scale.transform)(x1) + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if axis.inverted {
|
||||||
|
(x0, x1) = (x1, x0)
|
||||||
|
}
|
||||||
|
|
||||||
|
let k0 = (axis.scale.transform)(x0)
|
||||||
|
let k1 = (axis.scale.transform)(x1)
|
||||||
|
let D = k1 - k0
|
||||||
|
|
||||||
|
if not tight0 {
|
||||||
|
x0 = (axis.scale.inverse)(k0 - D * lower-margin/100%)
|
||||||
|
}
|
||||||
|
if not tight1 {
|
||||||
|
x1 = (axis.scale.inverse)(k1 + D * upper-margin/100%)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (x0, x1)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Generates all ticks and subticks as well as their labels for an axis.
|
||||||
|
///
|
||||||
|
/// -> dictionary
|
||||||
|
#let _axis-generate-ticks(
|
||||||
|
/// The axis object.
|
||||||
|
/// -> lq.axis
|
||||||
|
axis,
|
||||||
|
|
||||||
|
/// The length with which the axis will be displayed. This is for example used to determine automatic tick distances.
|
||||||
|
/// -> length
|
||||||
|
length: 3cm
|
||||||
|
) = {
|
||||||
|
let ticks = ()
|
||||||
|
let tick-labels
|
||||||
|
let subticks = ()
|
||||||
|
let subtick-labels
|
||||||
|
let (exp, offset) = (axis.exponent, axis.offset)
|
||||||
|
|
||||||
|
let em = measure(line(length: 1em, angle: 0deg)).width
|
||||||
|
axis.tick-args.num-ticks-suggestion = match(
|
||||||
|
axis.kind,
|
||||||
|
"x", length / (3.3 * em),
|
||||||
|
"y", length / (2 * em)
|
||||||
|
)
|
||||||
|
let (x0, x1) = axis.lim
|
||||||
|
|
||||||
|
|
||||||
|
if x1 < x0 {
|
||||||
|
// (x1, x0) = (x0, x1)
|
||||||
|
} else if x0 == x1 {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "Cannot generate ticks for empty range [" + str(x0) +", " + str(x1) + "]"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if axis.locate-ticks != none {
|
||||||
|
|
||||||
|
let tick-result = (axis.locate-ticks)(x0, x1, ..axis.tick-args)
|
||||||
|
ticks = tick-result.ticks
|
||||||
|
|
||||||
|
let format-result = if axis.format-ticks != none {
|
||||||
|
(axis.format-ticks)(
|
||||||
|
tick-result.ticks,
|
||||||
|
tick-info: tick-result,
|
||||||
|
exponent: axis.exponent,
|
||||||
|
offset: if type(offset) in (int, float, auto) { offset } else { 0 },
|
||||||
|
auto-exponent-threshold: axis.auto-exponent-threshold,
|
||||||
|
min: x0,
|
||||||
|
max: x1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(format-result) == array {
|
||||||
|
tick-labels = format-result
|
||||||
|
} else if type(format-result) == dictionary {
|
||||||
|
assertations.assert-dict-keys(
|
||||||
|
format-result,
|
||||||
|
mandatory: ("labels",),
|
||||||
|
optional: ("offset", "exponent")
|
||||||
|
)
|
||||||
|
tick-labels = format-result.labels
|
||||||
|
if "exponent" in format-result {
|
||||||
|
exp = format-result.exponent
|
||||||
|
}
|
||||||
|
if "offset" in format-result and offset == auto {
|
||||||
|
offset = format-result.offset
|
||||||
|
}
|
||||||
|
} else if format-result != none {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "The tick formatter must either return an array of labels or a dictionary with the keys \"labels\", \"exponent\", and \"offset\". Found " + repr(format-result)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if exp == auto { exp = 0 }
|
||||||
|
if offset == auto { offset = 0 }
|
||||||
|
|
||||||
|
if axis.locate-subticks != none {
|
||||||
|
let subtick-result = (axis.locate-subticks)(x0, x1, ..tick-result, ..axis.subtick-args)
|
||||||
|
|
||||||
|
subticks = subtick-result.ticks
|
||||||
|
subtick-labels = if axis.format-subticks != none {
|
||||||
|
(axis.format-subticks)(
|
||||||
|
subticks,
|
||||||
|
tick-info: subtick-result,
|
||||||
|
exponent: axis.exponent,
|
||||||
|
offset: axis.offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if type(subtick-labels) == dictionary {
|
||||||
|
subtick-labels = subtick-labels.labels
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
ticks: ticks,
|
||||||
|
tick-labels: tick-labels,
|
||||||
|
subticks: subticks,
|
||||||
|
subtick-labels: subtick-labels,
|
||||||
|
exp: exp,
|
||||||
|
offset: offset
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Draws an axis and its mirror (if any)
|
||||||
|
#let draw-axis(
|
||||||
|
axis,
|
||||||
|
tick-info,
|
||||||
|
orthogonal-axis-transform: none,
|
||||||
|
e-get: none
|
||||||
|
) = {
|
||||||
|
if axis.hidden { return (none, ()) }
|
||||||
|
|
||||||
|
let (ticks, tick-labels, subticks, subtick-labels, exp, offset) = tick-info
|
||||||
|
|
||||||
|
|
||||||
|
// Places a set of ticks together with labels
|
||||||
|
let place-ticks(
|
||||||
|
ticks,
|
||||||
|
labels,
|
||||||
|
// Where to place the ticks on the diagram
|
||||||
|
position,
|
||||||
|
// Whether to show tick labels
|
||||||
|
display-tick-labels,
|
||||||
|
sub: false,
|
||||||
|
kind: "x",
|
||||||
|
extra-ticks: ()
|
||||||
|
) = {
|
||||||
|
if labels == none { labels = (none,) * ticks.len() }
|
||||||
|
|
||||||
|
let align = position.inv()
|
||||||
|
let pad = e-get(lq-tick).pad
|
||||||
|
let factor = if sub { 1 - (e-get(lq-tick).shorten-sub / 100%) } else { 1 }
|
||||||
|
let outset = e-get(lq-tick).outset * factor
|
||||||
|
let shorten-sub = e-get(lq-tick).shorten-sub
|
||||||
|
let length = e-get(lq-tick).inset * factor + outset
|
||||||
|
let angle = if align in (top, bottom) { 90deg } else { 0deg }
|
||||||
|
|
||||||
|
let tick-stroke = if-none(
|
||||||
|
merge-strokes(
|
||||||
|
e-get(lq-tick).stroke,
|
||||||
|
axis.stroke, (cap: "butt"),
|
||||||
|
e-get(spine).stroke
|
||||||
|
),
|
||||||
|
0.5pt // can be none when spine.stroke is none
|
||||||
|
)
|
||||||
|
|
||||||
|
let tline = line(length: length, angle: angle, stroke: tick-stroke)
|
||||||
|
let make-tick
|
||||||
|
|
||||||
|
if align == right {
|
||||||
|
make-tick = (label, loc) => place(dx: -outset, dy: loc, {tline + place(dx: -length - pad, right + horizon, label)})
|
||||||
|
} else if align == left {
|
||||||
|
make-tick = (label, loc) => place(dx: -length + outset, dy: loc, {tline + place(dx: length + pad, left + horizon, label)})
|
||||||
|
} else if align == top {
|
||||||
|
make-tick = (label, loc) => place(dy: -length + outset, dx: loc, {tline + place(dy: length + pad, top + center, label)});
|
||||||
|
} else if align == bottom {
|
||||||
|
make-tick = (label, loc) => place(dy: -outset, dx: loc, {tline + place(dy: -length - pad, bottom + center, label)})
|
||||||
|
}
|
||||||
|
|
||||||
|
let max-value = (axis.transform)(axis.lim.at(if kind == "x" { 1 } else { 0 }))
|
||||||
|
|
||||||
|
let lq-tick-label = lq-tick-label.with(sub: sub, kind: kind)
|
||||||
|
|
||||||
|
labels = labels.map(label => {
|
||||||
|
if display-tick-labels {
|
||||||
|
lq-tick-label(label)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
let content = ticks.zip(labels).map(
|
||||||
|
((tick, label)) => {
|
||||||
|
let loc = (axis.transform)(tick)
|
||||||
|
if (axis.filter)(tick, calc.min(loc, max-value - loc)) {
|
||||||
|
make-tick(label, loc)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).join()
|
||||||
|
|
||||||
|
|
||||||
|
for tick in extra-ticks {
|
||||||
|
let format-ticks = if-none(
|
||||||
|
axis.format-ticks,
|
||||||
|
tick-format.linear
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(tick) in (int, float) {
|
||||||
|
tick = lq-tick(
|
||||||
|
tick,
|
||||||
|
label: format-ticks((tick,)).labels.first(),
|
||||||
|
align: position.inv(),
|
||||||
|
kind: axis.kind
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let label = e.fields(tick).at("label", default: none)
|
||||||
|
if label != none { labels.push(label) }
|
||||||
|
|
||||||
|
let loc = (axis.transform)(e.fields(tick).value)
|
||||||
|
let offset = if kind == "x" { (dx: loc) } else { (dy: loc) }
|
||||||
|
content += place(..offset, {
|
||||||
|
show: e.set_(lq-tick, align: position.inv(), kind: axis.kind)
|
||||||
|
show e.selector(lq-tick-label): it => {
|
||||||
|
if display-tick-labels { it }
|
||||||
|
}
|
||||||
|
tick
|
||||||
|
})
|
||||||
|
} // end extra-ticks
|
||||||
|
|
||||||
|
|
||||||
|
let max-padding = outset
|
||||||
|
let max-width = 0pt
|
||||||
|
|
||||||
|
let size = measure(lq-tick-label[asd])
|
||||||
|
|
||||||
|
if display-tick-labels {
|
||||||
|
let dimension = if axis.kind == "x" { "height" } else { "width" }
|
||||||
|
|
||||||
|
let label-space
|
||||||
|
(label-space, max-width) = labels
|
||||||
|
.map(label => {
|
||||||
|
let measured = measure(label)
|
||||||
|
(measured.at(dimension), measured.width)
|
||||||
|
})
|
||||||
|
.fold(
|
||||||
|
(0pt, 0pt),
|
||||||
|
((max-dim, max-w), (next-dim, next-w)) => (calc.max(max-dim, next-dim), calc.max(max-w, next-w)),
|
||||||
|
)
|
||||||
|
|
||||||
|
max-padding += label-space
|
||||||
|
if label-space > 0pt {
|
||||||
|
max-padding += pad
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prevent ticks from line-wrapping by boxing them in enough space.
|
||||||
|
if kind == "x" {
|
||||||
|
content = place(box(width: max-width, content))
|
||||||
|
} else {
|
||||||
|
content = place(box(width: max-padding, content))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (content, max-padding.to-absolute())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Draws a single axis (*or* a mirror)
|
||||||
|
let the-axis(
|
||||||
|
position: axis.position,
|
||||||
|
display-ticks: true,
|
||||||
|
display-tick-labels: true,
|
||||||
|
display-axis-label: true,
|
||||||
|
kind: "x"
|
||||||
|
) = {
|
||||||
|
let content = none
|
||||||
|
let space = 0pt
|
||||||
|
let bounds = ()
|
||||||
|
|
||||||
|
// Draw spine
|
||||||
|
if axis.stroke != none {
|
||||||
|
// TODO (later): use set rules here
|
||||||
|
let args = (:)
|
||||||
|
if axis.tip != auto { args.tip = axis.tip }
|
||||||
|
if axis.toe != auto { args.toe = axis.toe }
|
||||||
|
if axis.stroke != auto { args.stroke = axis.stroke }
|
||||||
|
content += place(spine(kind: axis.kind, ..args))
|
||||||
|
}
|
||||||
|
|
||||||
|
if display-ticks {
|
||||||
|
let (tick-content, tick-space) = place-ticks(
|
||||||
|
ticks, tick-labels, position, display-tick-labels, kind: kind, extra-ticks: axis.extra-ticks
|
||||||
|
)
|
||||||
|
content += tick-content
|
||||||
|
let (subtick-content, subtick-space) = place-ticks(
|
||||||
|
subticks, subtick-labels, position, display-tick-labels,
|
||||||
|
sub: true, kind: kind
|
||||||
|
)
|
||||||
|
space = calc.max(tick-space, subtick-space)
|
||||||
|
content += subtick-content
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if display-tick-labels {
|
||||||
|
|
||||||
|
let attachment = none
|
||||||
|
if type(offset) in (int, float) and offset != 0 {
|
||||||
|
attachment += zero.num(positive-sign: true, offset)
|
||||||
|
} else if offset not in (0, auto) {
|
||||||
|
attachment += offset
|
||||||
|
}
|
||||||
|
if type(exp) == int and exp != 0 {
|
||||||
|
attachment += {
|
||||||
|
show "X": none
|
||||||
|
zero.num("Xe" + str(exp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if attachment != none {
|
||||||
|
let args
|
||||||
|
if axis.kind == "x" {
|
||||||
|
args = (
|
||||||
|
dx: .5em, dy: 0pt,
|
||||||
|
alignment: bottom + right,
|
||||||
|
content-alignment: horizon + left
|
||||||
|
)
|
||||||
|
if position == top {
|
||||||
|
args.dy -= 100%
|
||||||
|
}
|
||||||
|
} else if axis.kind == "y" {
|
||||||
|
args = (
|
||||||
|
dx: 0pt, dy: -.5em,
|
||||||
|
alignment: top + left,
|
||||||
|
content-alignment: center + bottom
|
||||||
|
)
|
||||||
|
if position == right {
|
||||||
|
args.dx += 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (attachment-content, attachment-bounds) = place-with-bounds(
|
||||||
|
attachment,
|
||||||
|
..args
|
||||||
|
)
|
||||||
|
content += attachment-content
|
||||||
|
bounds.push(attachment-bounds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if axis.label != none and display-axis-label {
|
||||||
|
let label = axis.label
|
||||||
|
if e.eid(label) != e.eid(lq-label) {
|
||||||
|
let constructor = if kind == "x" { xlabel } else { ylabel }
|
||||||
|
label = constructor(label)
|
||||||
|
}
|
||||||
|
|
||||||
|
let wrap-label = if position in (top, bottom) {
|
||||||
|
box.with(width: 100%)
|
||||||
|
} else if position in (left, right) {
|
||||||
|
box.with(height: 100%)
|
||||||
|
}
|
||||||
|
|
||||||
|
let get-settable-field(element, object, field) = {
|
||||||
|
e.fields(object).at(field, default: e-get(element).at(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
let dx = get-settable-field(lq-label, label, "dx")
|
||||||
|
let dy = get-settable-field(lq-label, label, "dy")
|
||||||
|
let pad = get-settable-field(lq-label, label, "pad")
|
||||||
|
|
||||||
|
if pad == none {
|
||||||
|
pad = 0pt
|
||||||
|
} else {
|
||||||
|
pad = pad.to-absolute() + space
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let body = wrap-label(label)
|
||||||
|
let size = measure(body)
|
||||||
|
|
||||||
|
let (label-content, _) = place-with-bounds(
|
||||||
|
body, alignment: position, dx: dx, dy: dy, pad: pad
|
||||||
|
)
|
||||||
|
|
||||||
|
content += label-content
|
||||||
|
|
||||||
|
if kind == "x" {
|
||||||
|
space = calc.max(space, size.height + pad)
|
||||||
|
} else {
|
||||||
|
space = calc.max(space, size.width + pad)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// define box of axis spine
|
||||||
|
content += block(
|
||||||
|
..if kind == "y" { (height: 100%, width: 0%) }
|
||||||
|
else { (width: 100%, height: 0pt) },
|
||||||
|
inset: 0pt, outset: 0pt
|
||||||
|
)
|
||||||
|
|
||||||
|
let main-bounds = create-bounds()
|
||||||
|
let inset = e-get(lq-tick).inset
|
||||||
|
if axis.kind == "x" {
|
||||||
|
main-bounds.right = 100%
|
||||||
|
if position == top {
|
||||||
|
main-bounds.top = -space
|
||||||
|
main-bounds.bottom = inset
|
||||||
|
} else if position == bottom {
|
||||||
|
main-bounds.bottom = 100% + space
|
||||||
|
main-bounds.top = 100% - inset
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
main-bounds.bottom = 100%
|
||||||
|
if position == left {
|
||||||
|
main-bounds.left = -space
|
||||||
|
main-bounds.right = inset
|
||||||
|
} else if position == right {
|
||||||
|
main-bounds.right = 100% + space
|
||||||
|
main-bounds.left = 100% - inset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bounds.push(main-bounds)
|
||||||
|
return (content, bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let orthogonal-offset = axis.orthogonal-offset
|
||||||
|
if type(orthogonal-offset) in (int, float) {
|
||||||
|
orthogonal-offset = orthogonal-axis-transform(orthogonal-offset)
|
||||||
|
|
||||||
|
if axis.position in (bottom, right) {
|
||||||
|
orthogonal-offset -= 100%
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if axis.kind == "x" {
|
||||||
|
orthogonal-offset = (0pt, orthogonal-offset)
|
||||||
|
} else {
|
||||||
|
orthogonal-offset = (orthogonal-offset, 0pt)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let (axis-content, bounds) = the-axis(kind: axis.kind)
|
||||||
|
let content = place(
|
||||||
|
axis.position,
|
||||||
|
axis-content,
|
||||||
|
dx: orthogonal-offset.at(0),
|
||||||
|
dy: orthogonal-offset.at(1)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if axis.mirror != none {
|
||||||
|
let (mirror-axis-content, mirror-axis-bounds) = the-axis(
|
||||||
|
kind: axis.kind,
|
||||||
|
position: axis.position.inv(),
|
||||||
|
display-ticks: axis.mirror.at("ticks", default: false),
|
||||||
|
display-tick-labels: axis.mirror.at("tick-labels", default: false),
|
||||||
|
display-axis-label: axis.mirror.at("label", default: false),
|
||||||
|
)
|
||||||
|
content += place(axis.position.inv(), mirror-axis-content)
|
||||||
|
bounds += mirror-axis-bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
return (content, bounds.map(b => offset-bounds(b, orthogonal-offset)))
|
||||||
|
}
|
||||||
123
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/colorbar.typ
vendored
Normal file
123
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/colorbar.typ
vendored
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
#import "diagram.typ": diagram
|
||||||
|
#import "../plot/rect.typ": rect
|
||||||
|
|
||||||
|
#import "../typing.typ": set-diagram
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a visual representation of the color mapping used in a plot
|
||||||
|
/// instance like @scatter or @colormesh.
|
||||||
|
///
|
||||||
|
/// This generates a new (usually slim) diagram with a filled gradient
|
||||||
|
/// according to the color map used in the plot, appropriate ticks, and
|
||||||
|
/// optionally a label. This diagram can be configured through general `set`
|
||||||
|
/// rules on @diagram and through additional arguments passed through
|
||||||
|
/// @colorbar.args.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #show: lq.set-diagram(height: 3.5cm, width: 4cm)
|
||||||
|
///
|
||||||
|
/// #let mesh = lq.colormesh(
|
||||||
|
/// lq.linspace(-0.3, 1.3),
|
||||||
|
/// lq.linspace(-0.3, 1.3),
|
||||||
|
/// (x, y) => x * y,
|
||||||
|
/// map: gradient.linear(..color.map.icefire).sharp(9)
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// mesh
|
||||||
|
/// )
|
||||||
|
/// #lq.colorbar(mesh, thickness: 2mm)
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The color bar is a separate inline object and can be placed anywhere in the
|
||||||
|
/// document. Note that if the example above were in code mode, Typst would
|
||||||
|
/// place the color bar directly after the diagram with no space in-between.
|
||||||
|
/// You can insert a manual space through `h(.5em)` (or similar).
|
||||||
|
///
|
||||||
|
/// Also, the color bar has a fixed length by default and does not know about
|
||||||
|
/// the dimensions of the previous diagram. In order to guarentee the same
|
||||||
|
/// height, it is advisable to use a set rule on `diagram` (as demonstrated
|
||||||
|
/// above) to set the height for both the diagram and the color bar at once.
|
||||||
|
#let colorbar(
|
||||||
|
|
||||||
|
/// A plot instance that uses color-coding, e.g., @scatter, @colormesh, @contour, and @quiver.
|
||||||
|
/// -> plot
|
||||||
|
plot,
|
||||||
|
|
||||||
|
|
||||||
|
/// How to orient the colorbar.
|
||||||
|
/// -> "vertical" | "horizontal"
|
||||||
|
orientation: "vertical",
|
||||||
|
|
||||||
|
/// The thickness of the colorbar.
|
||||||
|
/// -> length
|
||||||
|
thickness: 3mm,
|
||||||
|
|
||||||
|
/// A label to place on the axis.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Additional arguments to pass to @diagram.
|
||||||
|
/// -> any
|
||||||
|
..args
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let cinfo = plot.cinfo
|
||||||
|
|
||||||
|
let grad = if orientation == "vertical" {
|
||||||
|
rect(
|
||||||
|
0%, cinfo.min,
|
||||||
|
width: 100%,
|
||||||
|
height: cinfo.max - cinfo.min,
|
||||||
|
fill: gradient.linear(
|
||||||
|
..cinfo.colormap.stops(),
|
||||||
|
angle: 90deg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else if orientation == "horizontal" {
|
||||||
|
rect(
|
||||||
|
cinfo.min, 0%,
|
||||||
|
height: 100%,
|
||||||
|
width: cinfo.max - cinfo.min,
|
||||||
|
fill: gradient.linear(
|
||||||
|
..cinfo.colormap.stops(),
|
||||||
|
angle: 0deg
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
show: set-diagram(
|
||||||
|
grid: none,
|
||||||
|
margin: 0%,
|
||||||
|
)
|
||||||
|
|
||||||
|
let preset-args = (:)
|
||||||
|
if orientation == "vertical" {
|
||||||
|
preset-args = (
|
||||||
|
width: thickness,
|
||||||
|
xaxis: (ticks: none),
|
||||||
|
yaxis: (position: right, mirror: (:)),
|
||||||
|
yscale: cinfo.norm,
|
||||||
|
ylabel: label
|
||||||
|
)
|
||||||
|
} else if orientation == "horizontal" {
|
||||||
|
preset-args = (
|
||||||
|
height: thickness,
|
||||||
|
yaxis: (ticks: none),
|
||||||
|
xaxis: (position: bottom, mirror: (:)),
|
||||||
|
xscale: cinfo.norm,
|
||||||
|
xlabel: label
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "Unexpected orientation \"" + orientation + "\", possible values are \"horizontal\" and \"vertical\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
diagram(
|
||||||
|
..preset-args,
|
||||||
|
..args,
|
||||||
|
grad
|
||||||
|
)
|
||||||
|
}
|
||||||
696
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/diagram.typ
vendored
Normal file
696
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/diagram.typ
vendored
Normal file
@@ -0,0 +1,696 @@
|
|||||||
|
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../utility.typ": if-auto
|
||||||
|
#import "../bounds.typ": update-bounds, place-with-bounds
|
||||||
|
#import "../process-styles.typ": update-stroke, process-margin, process-grid-arg, twod-ify-alignment
|
||||||
|
#import "../logic/process-coordinates.typ": transform-point
|
||||||
|
|
||||||
|
|
||||||
|
#import "legend.typ": legend as lq-legend, _place-legend-with-bounds
|
||||||
|
#import "grid.typ": grid as lq-grid
|
||||||
|
#import "title.typ": title as lq-title, _place-title-with-bounds
|
||||||
|
#import "label.typ": label as lq-label
|
||||||
|
#import "axis.typ": axis as lq-axis, draw-axis, _axis-compute-limits, _axis-generate-ticks
|
||||||
|
|
||||||
|
#import "../logic/transform.typ": create-trafo
|
||||||
|
|
||||||
|
#import "../style/styling.typ": init as cycle-init, style, process-cycles-arg
|
||||||
|
#import "../style/map.typ": petroff10
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
#let debug = false
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a new diagram.
|
||||||
|
///
|
||||||
|
/// -> lq.diagram
|
||||||
|
#let diagram(
|
||||||
|
|
||||||
|
/// The width of the diagram. This can be
|
||||||
|
/// - A `length`; in this case, it defines just the width of the data area,
|
||||||
|
/// excluding axes, labels, title etc.
|
||||||
|
/// - A `ratio` or `relative` where the ratio part is relative to the width
|
||||||
|
/// of the parent that the diagram is placed in. This is not allowed if the
|
||||||
|
/// parent has an unbounded width, e.g., a page with `width: auto`.
|
||||||
|
/// -> length | relative
|
||||||
|
width: 6cm,
|
||||||
|
|
||||||
|
/// The height of the diagram. This can be
|
||||||
|
/// - A `length`; in this case, it defines just the height of the data area,
|
||||||
|
/// excluding axes, labels, title etc.
|
||||||
|
/// - A `ratio` or `relative` where the ratio part is relative to the height
|
||||||
|
/// of the parent that the diagram is placed in. This is not allowed if the
|
||||||
|
/// parent has an unbounded height, e.g., a page with `height: auto`.
|
||||||
|
/// -> length | relative
|
||||||
|
height: 4cm,
|
||||||
|
|
||||||
|
/// The title for the diagram. Use a @title object for more options.
|
||||||
|
/// -> lq.title | str | content | none
|
||||||
|
title: none,
|
||||||
|
|
||||||
|
/// Options to pass to the @legend constructor. If set to `none`, no legend is
|
||||||
|
/// shown.
|
||||||
|
///
|
||||||
|
/// Alternatively, a legend with entirely custom entries can be created and
|
||||||
|
/// given here.
|
||||||
|
/// -> none | dictionary | lq.legend
|
||||||
|
legend: (:),
|
||||||
|
|
||||||
|
/// Data limits along the $x$-axis. Expects `auto` or a tuple `(min, max)`
|
||||||
|
/// where `min` and `max` may individually be `auto`. Also see @axis.lim.
|
||||||
|
/// -> auto | array
|
||||||
|
xlim: auto,
|
||||||
|
|
||||||
|
/// Data limits along the $y$-axis. Expects `auto` or a tuple `(min, max)`
|
||||||
|
/// where `min` and `max` may individually be `auto`. Also see @axis.lim.
|
||||||
|
/// -> auto | array
|
||||||
|
ylim: auto,
|
||||||
|
|
||||||
|
/// Label for the $x$-axis. Use a @label object for more options.
|
||||||
|
/// -> lq.label | content
|
||||||
|
xlabel: none,
|
||||||
|
|
||||||
|
/// Label for the $y$-axis. Use a @label object for more options.
|
||||||
|
/// -> lq.label | content
|
||||||
|
ylabel: none,
|
||||||
|
|
||||||
|
/// Options to apply to the grid. A `stroke`, `color`, or `length` argument
|
||||||
|
/// directly sets the grid stroke while a `dictionary` with the possible keys
|
||||||
|
/// `stroke`, `stroke-sub`, and `z-index` gives more fine-grained control.
|
||||||
|
/// Setting this parameter to `none` removes the grid entirely.
|
||||||
|
/// See @grid for more details.
|
||||||
|
/// -> auto | none | dictionary | stroke | color | length
|
||||||
|
grid: auto,
|
||||||
|
|
||||||
|
/// Sets the scale of the $x$-axis. This may be a @scale object or the name
|
||||||
|
/// of one of the built-in scales `"linear"`, `"log"`, `"symlog"`, and
|
||||||
|
/// `"datetime"`.
|
||||||
|
///
|
||||||
|
/// If left at `auto`, the scale will be set to `"datetime"` if any of the
|
||||||
|
/// plots uses datetime coordinates and `"linear"` otherwise.
|
||||||
|
/// -> auto | str | lq.scale
|
||||||
|
xscale: auto,
|
||||||
|
|
||||||
|
/// Sets the scale of the $y$-axis. This may be a @scale object or the name
|
||||||
|
/// of one of the built-in scales `"linear"`, `"log"`, `"symlog"`, and
|
||||||
|
/// `"datetime"`.
|
||||||
|
///
|
||||||
|
/// If left at `auto`, the scale will be set to `"datetime"` if any of the
|
||||||
|
/// plots uses datetime coordinates and `"linear"` otherwise.
|
||||||
|
/// -> auto | str | lq.scale
|
||||||
|
yscale: auto,
|
||||||
|
|
||||||
|
/// Configures the $x$-axis through a dictionary of arguments to pass to the
|
||||||
|
/// constructor of the axis. See @axis for available options.
|
||||||
|
/// -> none | dictionary
|
||||||
|
xaxis: (:),
|
||||||
|
|
||||||
|
/// Configures the $y$-axis through a dictionary of arguments to pass to the
|
||||||
|
/// constructor of the axis. See @axis for available options.
|
||||||
|
/// -> none | dictionary
|
||||||
|
yaxis: (:),
|
||||||
|
|
||||||
|
/// Configures the automatic margins of the diagram around the data. If set
|
||||||
|
/// to `0%`, the outer-most data points align tightly with the edges of the
|
||||||
|
/// diagram (as long as the axis limits are left at `auto`). Otherwise, the
|
||||||
|
/// margins are computed in percent of the covered range along an axis (in
|
||||||
|
/// scaled coordinates).
|
||||||
|
///
|
||||||
|
/// The margins can be set individually for each side by passing a dictionary
|
||||||
|
/// with the possible keys
|
||||||
|
/// - `left`, `right`, `top`, `bottom` for addressing individual sides,
|
||||||
|
/// - `x`, `y` for left/right and top/bottom combined sides, and
|
||||||
|
/// - `rest` for all sides not specified by any of the above.
|
||||||
|
///
|
||||||
|
/// -> ratio | dictionary
|
||||||
|
margin: 6%,
|
||||||
|
|
||||||
|
/// Style cycle to use for this diagram. Check out the
|
||||||
|
/// #link("tutorials/cycles")[cycles tutorial] for more information.
|
||||||
|
/// The elements of a cycle array should either be
|
||||||
|
/// - all functions as described in the tutorial, or
|
||||||
|
/// - all of type `color` (e.g., one of the maps under `lq.color.map`), or
|
||||||
|
/// - all of type `dictionary` with possible keys `color`, `stroke`, and
|
||||||
|
/// `mark`.
|
||||||
|
///
|
||||||
|
/// -> array
|
||||||
|
cycle: petroff10,
|
||||||
|
|
||||||
|
/// How to fill the background of the data area.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// Plot objects like @plot, @bar, @scatter, @contour etc. and additional
|
||||||
|
/// @axis objects.
|
||||||
|
/// -> any
|
||||||
|
..children
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let create-principle-axis(
|
||||||
|
axis, lim, scale,
|
||||||
|
kind: "x",
|
||||||
|
label,
|
||||||
|
plots,
|
||||||
|
margin,
|
||||||
|
it
|
||||||
|
) = {
|
||||||
|
if axis == none { axis = (hidden: true) }
|
||||||
|
|
||||||
|
if type(axis) == dictionary {
|
||||||
|
axis = lq-axis(kind: kind, label: label, scale: scale, lim: lim, ..axis, ..plots)
|
||||||
|
}
|
||||||
|
|
||||||
|
let margin = if kind == "x" {
|
||||||
|
(lower-margin: margin.left, upper-margin: margin.right)
|
||||||
|
} else {
|
||||||
|
(lower-margin: margin.bottom, upper-margin: margin.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
axis.lim = _axis-compute-limits(axis, is-independant: true, ..margin)
|
||||||
|
|
||||||
|
|
||||||
|
let normalized-trafo = create-trafo(axis.scale.transform, ..axis.lim)
|
||||||
|
axis.normalized-transform = normalized-trafo
|
||||||
|
|
||||||
|
axis
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let fill-in-transforms(axes, width, height) = {
|
||||||
|
let xaxis = axes.at(0)
|
||||||
|
let yaxis = axes.at(1)
|
||||||
|
axes.map(axis => {
|
||||||
|
let normalized-trafo = if axis.plots.len() > 0 { // is independent axis
|
||||||
|
create-trafo(axis.scale.transform, ..axis.lim)
|
||||||
|
} else { // is dependent axis
|
||||||
|
let model-axis = if axis.kind == "x" { xaxis } else { yaxis }
|
||||||
|
a => (model-axis.normalized-transform)((axis.functions.inv)(a))
|
||||||
|
}
|
||||||
|
|
||||||
|
axis.transform = if axis.kind == "x" {
|
||||||
|
x => normalized-trafo(x) * width
|
||||||
|
} else {
|
||||||
|
y => (1 - normalized-trafo(y)) * height
|
||||||
|
}
|
||||||
|
axis
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#let generate-grid(axis-info, xaxis, yaxis, grid: auto) = {
|
||||||
|
grid = process-grid-arg(grid)
|
||||||
|
|
||||||
|
lq-grid(
|
||||||
|
axis-info.x.subticks.map(xaxis.transform),
|
||||||
|
sub: true,
|
||||||
|
kind: "x",
|
||||||
|
..grid
|
||||||
|
)
|
||||||
|
lq-grid(
|
||||||
|
axis-info.y.subticks.map(yaxis.transform),
|
||||||
|
sub: true,
|
||||||
|
kind: "y",
|
||||||
|
..grid
|
||||||
|
)
|
||||||
|
lq-grid(
|
||||||
|
axis-info.x.ticks.map(xaxis.transform),
|
||||||
|
sub: false,
|
||||||
|
kind: "x",
|
||||||
|
..grid
|
||||||
|
)
|
||||||
|
lq-grid(
|
||||||
|
axis-info.y.ticks.map(yaxis.transform),
|
||||||
|
sub: false,
|
||||||
|
kind: "y",
|
||||||
|
..grid
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let generate-legend(legend, legend-entries, e-get) = {
|
||||||
|
if legend != none and (legend-entries.len() > 0 or e.eid(legend) == e.eid(lq-legend)) {
|
||||||
|
let (legend-content, legend-bounds) = _place-legend-with-bounds(
|
||||||
|
legend, legend-entries, e-get
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
content: legend-content,
|
||||||
|
bounds: legend-bounds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let generate-plots(
|
||||||
|
plots, cycle, width, height, axes, only-bounds: false
|
||||||
|
) = {
|
||||||
|
let (xaxis, yaxis) = axes.slice(0, 2)
|
||||||
|
|
||||||
|
let transform(x, y) = (
|
||||||
|
(xaxis.transform)(x),
|
||||||
|
(yaxis.transform)(y),
|
||||||
|
)
|
||||||
|
cycle = process-cycles-arg(cycle)
|
||||||
|
|
||||||
|
let update-bounds = update-bounds.with(width: width, height: height)
|
||||||
|
|
||||||
|
let bounds = (left: 0pt, right: width, top: 0pt, bottom: height)
|
||||||
|
let artists = ()
|
||||||
|
let legend-entries = ()
|
||||||
|
let cycle-index = 0
|
||||||
|
|
||||||
|
for plot in plots {
|
||||||
|
let transform = transform
|
||||||
|
|
||||||
|
if type(plot) == dictionary and "axis-id" in plot {
|
||||||
|
let axis = axes.at(plot.axis-id + 2)
|
||||||
|
transform = if axis.kind == "x" {
|
||||||
|
(x, y) => ((axis.transform)(x), (yaxis.transform)(y))
|
||||||
|
} else {
|
||||||
|
(x, y) => ((xaxis.transform)(x), (axis.transform)(y))
|
||||||
|
}
|
||||||
|
plot = plot.plot
|
||||||
|
}
|
||||||
|
|
||||||
|
if plot.at("id", default: none) == "place" and not plot.clip {
|
||||||
|
let (px, py) = transform-point(plot.x, plot.y, transform)
|
||||||
|
let (_, place-bounds) = place-with-bounds(
|
||||||
|
plot.body, dx: px, dy: py,
|
||||||
|
content-alignment: twod-ify-alignment(plot.align)
|
||||||
|
)
|
||||||
|
bounds = update-bounds(bounds, place-bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let takes-part-in-cycle = not plot.at("ignores-cycle", default: true)
|
||||||
|
let cycle-style = cycle.at(calc.rem(cycle-index, cycle.len()))
|
||||||
|
|
||||||
|
if not only-bounds {
|
||||||
|
let plotted-plot = {
|
||||||
|
show: cycle-init
|
||||||
|
show: cycle-style
|
||||||
|
(plot.plot)(plot, transform)
|
||||||
|
}
|
||||||
|
|
||||||
|
if takes-part-in-cycle {
|
||||||
|
cycle-index += 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if plot.at("clip", default: true) {
|
||||||
|
plotted-plot = place(
|
||||||
|
box(width: width, height: height, clip: true, plotted-plot)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
artists.push((content: plotted-plot, z: plot.at("z-index", default: 2)))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if "legend" in plot and plot.label != none {
|
||||||
|
plot.make-legend = true
|
||||||
|
let legend-trafo(x, y) = {
|
||||||
|
(x * 100%, (1 - y) * 100%)
|
||||||
|
}
|
||||||
|
let handle = {
|
||||||
|
show: cycle-init
|
||||||
|
show: cycle-style
|
||||||
|
(plot.plot)(plot, legend-trafo)
|
||||||
|
}
|
||||||
|
legend-entries.push((
|
||||||
|
box(width: 2em, height: .7em, handle),
|
||||||
|
plot.label
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
legend-entries: legend-entries,
|
||||||
|
artists: artists,
|
||||||
|
bounds: bounds
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let attempt-layout(
|
||||||
|
width, height,
|
||||||
|
it: (:), axes: (), plots: (), e-get: none,
|
||||||
|
auto-height: true, auto-width: true,
|
||||||
|
available-size: (0pt, 0pt)
|
||||||
|
) = {
|
||||||
|
axes = fill-in-transforms(axes, width, height)
|
||||||
|
let (xaxis, yaxis) = axes.slice(0, 2)
|
||||||
|
|
||||||
|
let get-settable-field(element, object, field) = {
|
||||||
|
e.fields(object).at(field, default: e-get(element).at(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
let bounds = (left: 0pt, right: width, top: 0pt, bottom: height)
|
||||||
|
let update-bounds = update-bounds.with(width: width, height: height)
|
||||||
|
|
||||||
|
let tickings = axes.map(axis =>
|
||||||
|
_axis-generate-ticks(
|
||||||
|
axis,
|
||||||
|
length: if axis.kind == "x" { width } else { height }
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
for (axis, ticking) in axes.zip(tickings) {
|
||||||
|
let (_, axis-bounds) = draw-axis(
|
||||||
|
axis, ticking, e-get: e-get, orthogonal-axis-transform: (if axis.kind == "x" { yaxis} else {xaxis}).transform
|
||||||
|
)
|
||||||
|
bounds = axis-bounds.fold(bounds, update-bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if it.title != none {
|
||||||
|
let (_, title-bounds) = _place-title-with-bounds(
|
||||||
|
it.title, get-settable-field, width, height
|
||||||
|
)
|
||||||
|
bounds = update-bounds(bounds, title-bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (legend-entries, bounds: plot-bounds) = generate-plots(
|
||||||
|
plots, it.cycle, width, height,
|
||||||
|
axes, only-bounds: true
|
||||||
|
)
|
||||||
|
bounds = update-bounds(bounds, plot-bounds)
|
||||||
|
|
||||||
|
let legend = generate-legend(it.legend, legend-entries, e-get)
|
||||||
|
if legend != none {
|
||||||
|
bounds = update-bounds(bounds, legend.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auto-width {
|
||||||
|
width = available-size.width - bounds.right + bounds.left + width
|
||||||
|
}
|
||||||
|
if auto-height {
|
||||||
|
height = available-size.height - bounds.bottom + bounds.top + height
|
||||||
|
}
|
||||||
|
return (width, height, tickings)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let show-bounds(bounds, clr: red) = {
|
||||||
|
if not debug { return none }
|
||||||
|
place(dx: bounds.left, dy: bounds.top, rect(width: bounds.right - bounds.left, height: bounds.bottom - bounds.top, fill: clr))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let draw-diagram(it) = {
|
||||||
|
set math.equation(numbering: none)
|
||||||
|
set curve(stroke: .7pt)
|
||||||
|
set line(stroke: .7pt)
|
||||||
|
|
||||||
|
|
||||||
|
// show: e.show_(
|
||||||
|
// lq-label.with(kind: "y"),
|
||||||
|
// it => {
|
||||||
|
// set rotate(-90deg)
|
||||||
|
// it
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
|
||||||
|
|
||||||
|
// Elements can be plot objects or plots on an axis: (axis-id: int, plot: dict).
|
||||||
|
let plots = ()
|
||||||
|
// solely used for computing limits
|
||||||
|
let (xplots, yplots) = ((), ())
|
||||||
|
// all additional axes
|
||||||
|
let axes = ()
|
||||||
|
|
||||||
|
for child in it.children {
|
||||||
|
if child == none { continue }
|
||||||
|
if type(child) != dictionary {
|
||||||
|
panic("Unexpected child `" + repr(child) + "`. Expected a plot or an axis. ")
|
||||||
|
}
|
||||||
|
|
||||||
|
if child.at("type", default: "") == "axis" { // is an axis
|
||||||
|
axes.push(child)
|
||||||
|
|
||||||
|
if child.plots.len() > 0 {
|
||||||
|
plots += child.plots.map(plot => (axis-id: axes.len() - 1, plot: plot))
|
||||||
|
if child.kind == "x" {
|
||||||
|
yplots += child.plots
|
||||||
|
} else {
|
||||||
|
xplots += child.plots
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else { // is a plot
|
||||||
|
yplots.push(child)
|
||||||
|
xplots.push(child)
|
||||||
|
plots.push(child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let margin = process-margin(it.margin)
|
||||||
|
|
||||||
|
let xaxis = create-principle-axis(
|
||||||
|
kind: "x",
|
||||||
|
it.xaxis, it.xlim, it.xscale,
|
||||||
|
it.xlabel, xplots, margin, it
|
||||||
|
)
|
||||||
|
let yaxis = create-principle-axis(
|
||||||
|
kind: "y",
|
||||||
|
it.yaxis, it.ylim, it.yscale,
|
||||||
|
it.ylabel, yplots, margin, it
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Compute limits for additional axes
|
||||||
|
for i in range(axes.len()) {
|
||||||
|
let axis = axes.at(i)
|
||||||
|
let axes-margin = if axis.kind == "x" {
|
||||||
|
(lower-margin: margin.left, upper-margin: margin.right)
|
||||||
|
} else {
|
||||||
|
(lower-margin: margin.bottom, upper-margin: margin.top)
|
||||||
|
}
|
||||||
|
|
||||||
|
let model-axis = if axis.kind == "x" { xaxis } else { yaxis }
|
||||||
|
axes.at(i).lim = _axis-compute-limits(
|
||||||
|
axis, default-lim: model-axis.lim, ..axes-margin
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tell additional axes how to transform their coordinates
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
e.get(e-get => {
|
||||||
|
let axes = (xaxis, yaxis) + axes
|
||||||
|
|
||||||
|
|
||||||
|
let get-settable-field(element, object, field) = {
|
||||||
|
e.fields(object).at(field, default: e-get(element).at(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let it = it
|
||||||
|
let tickings = ()
|
||||||
|
|
||||||
|
// Diagram may have relative/ratio width or height
|
||||||
|
if type(it.width) == relative or type(it.height) == relative {
|
||||||
|
let attempt-layout = attempt-layout.with(
|
||||||
|
auto-width: type(it.width) != length,
|
||||||
|
auto-height: type(it.height) != length,
|
||||||
|
available-size: it.size, it: it, axes: axes, e-get: e-get, plots: plots
|
||||||
|
)
|
||||||
|
|
||||||
|
let exact-or-guess(length, container-length) = {
|
||||||
|
if type(length) == std.length { length }
|
||||||
|
else { 0.9 * container-length * length.ratio + length.length.to-absolute() }
|
||||||
|
}
|
||||||
|
|
||||||
|
// First guess for diagram area
|
||||||
|
let (width, height, ..) = attempt-layout(
|
||||||
|
exact-or-guess(it.width, it.size.width),
|
||||||
|
exact-or-guess(it.height, it.size.height),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
// Now that we have a better guess for the size of the diagram area
|
||||||
|
// let us re-evaluate the ticking because maybe our initial guess was really bad.
|
||||||
|
// In this step, we expect the size not too change very substantially,
|
||||||
|
// so we fix the ticking now and re-use it again in the final layout step.
|
||||||
|
(it.width, it.height, tickings) = attempt-layout(width, height)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
tickings = axes.map(axis => _axis-generate-ticks(axis, length: if axis.kind == "x" { it.width } else { it.height }))
|
||||||
|
}
|
||||||
|
|
||||||
|
axes = fill-in-transforms(axes, it.width, it.height)
|
||||||
|
|
||||||
|
let (xaxis, yaxis) = axes.slice(0, 2)
|
||||||
|
|
||||||
|
let bounds = (left: 0pt, right: it.width, top: 0pt, bottom: it.height)
|
||||||
|
let update-bounds = update-bounds.with(width: it.width, height: it.height)
|
||||||
|
|
||||||
|
let diagram = box(
|
||||||
|
width: it.width, height: it.height,
|
||||||
|
inset: 0pt, outset: 0pt,
|
||||||
|
stroke: none, fill: it.fill,
|
||||||
|
{
|
||||||
|
set align(top + left) // sometimes alignment is messed up
|
||||||
|
set place(left) // important for RTL text direction
|
||||||
|
|
||||||
|
let artists = ()
|
||||||
|
|
||||||
|
|
||||||
|
// GRID
|
||||||
|
let axis-info = (
|
||||||
|
x: tickings.at(0),
|
||||||
|
y: tickings.at(1),
|
||||||
|
)
|
||||||
|
artists.push((
|
||||||
|
content: generate-grid(axis-info, xaxis, yaxis, grid: it.grid), z: e-get(lq-grid).z-index
|
||||||
|
))
|
||||||
|
|
||||||
|
|
||||||
|
// PLOTS
|
||||||
|
let (legend-entries, artists: plot-artists, bounds: plot-bounds) = generate-plots(
|
||||||
|
plots, it.cycle, it.width, it.height,
|
||||||
|
axes
|
||||||
|
)
|
||||||
|
artists += plot-artists
|
||||||
|
bounds = update-bounds(bounds, plot-bounds)
|
||||||
|
|
||||||
|
|
||||||
|
// AXES
|
||||||
|
for (axis, ticking) in axes.zip(tickings) {
|
||||||
|
let (axis-content, axis-bounds) = draw-axis(
|
||||||
|
axis, ticking, e-get: e-get,
|
||||||
|
orthogonal-axis-transform: (if axis.kind == "x" { yaxis} else {xaxis}).transform
|
||||||
|
)
|
||||||
|
artists.push((content: axis-content, z: 20))
|
||||||
|
|
||||||
|
axis-bounds.map(show-bounds.with(clr: rgb("#2222AA22"))).join()
|
||||||
|
bounds = axis-bounds.fold(bounds, update-bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// TITLE
|
||||||
|
if it.title != none {
|
||||||
|
let (title-content, title-bounds) = _place-title-with-bounds(
|
||||||
|
it.title, get-settable-field, it.width, it.height
|
||||||
|
)
|
||||||
|
|
||||||
|
artists.push((content: title-content, z: 20))
|
||||||
|
bounds = update-bounds(bounds, title-bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// LEGEND
|
||||||
|
let legend = generate-legend(it.legend, legend-entries, e-get)
|
||||||
|
if legend != none {
|
||||||
|
artists.push((content: legend.content, z: e-get(lq-legend).z-index))
|
||||||
|
bounds = update-bounds(bounds, legend.bounds)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
artists.sorted(key: artist => artist.z).map(artist => artist.content).join()
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
bounds.bottom -= it.height
|
||||||
|
bounds.right -= it.width
|
||||||
|
bounds.left *= -1
|
||||||
|
bounds.top *= -1
|
||||||
|
|
||||||
|
box(
|
||||||
|
inset: bounds,
|
||||||
|
diagram,
|
||||||
|
stroke: if debug { 0.1pt } else { none },
|
||||||
|
baseline: bounds.bottom
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let folding-dict = e.types.wrap(dictionary, fold: old-fold => (a, b) => a + b)
|
||||||
|
|
||||||
|
#let diagram = e.element.declare(
|
||||||
|
"diagram",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
if type(it.width) == relative or type(it.height) == relative {
|
||||||
|
box(layout(size => {
|
||||||
|
if type(it.width) == relative {
|
||||||
|
// We have an exception for 0% which is useful to fit the _entire_
|
||||||
|
// diagram to fixed dimensions.
|
||||||
|
assert(
|
||||||
|
size.width != float.inf * 1pt or it.width.ratio == 0%,
|
||||||
|
message: "Cannot create diagram with relative width (" +
|
||||||
|
repr(it.width) + ") placed in a container with automatic width"
|
||||||
|
)
|
||||||
|
size.width = size.width*it.width.ratio + it.width.length.to-absolute()
|
||||||
|
}
|
||||||
|
if type(it.height) == relative {
|
||||||
|
assert(
|
||||||
|
size.height != float.inf * 1pt or it.height.ratio == 0%,
|
||||||
|
message: "Cannot create diagram with relative height (" +
|
||||||
|
repr(it.height) + ") placed in a container with automatic height"
|
||||||
|
)
|
||||||
|
size.height = size.height*it.height.ratio + it.height.length.to-absolute()
|
||||||
|
}
|
||||||
|
draw-diagram(it + (size: size))
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
draw-diagram(it)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("children", e.types.any, required: true),
|
||||||
|
e.field("width", e.types.union(length, relative), default: 6cm),
|
||||||
|
e.field("height", e.types.union(length, relative), default: 4cm),
|
||||||
|
e.field("title", e.types.union(none, str, content, lq-title), default: none),
|
||||||
|
e.field("legend", e.types.option(e.types.union(dictionary, lq-legend)), default: (:)),
|
||||||
|
e.field("xlim", e.types.wrap(e.types.union(auto, array), fold: none), default: auto),
|
||||||
|
e.field("ylim", e.types.wrap(e.types.union(auto, array), fold: none), default: auto),
|
||||||
|
e.field("xlabel", e.types.option(e.types.union(lq-label, str, content)), default: none),
|
||||||
|
e.field("ylabel", e.types.option(e.types.union(lq-label, str, content)), default: none),
|
||||||
|
e.field("grid", e.types.union(auto, none, dictionary, stroke, color, length), default: auto),
|
||||||
|
e.field("xscale", e.types.union(auto, str, dictionary), default: auto),
|
||||||
|
e.field("yscale", e.types.union(auto, str, dictionary), default: auto),
|
||||||
|
e.field("xaxis", e.types.option(folding-dict), default: (:)),
|
||||||
|
e.field("yaxis", e.types.option(folding-dict), default: (:)),
|
||||||
|
e.field("margin", e.types.union(ratio, dictionary), default: 6%),
|
||||||
|
e.field("cycle", e.types.wrap(e.types.array(e.types.union(function, color, dictionary)), fold: none), default: petroff10),
|
||||||
|
e.field("fill", e.types.option(e.types.paint), default: none),
|
||||||
|
),
|
||||||
|
|
||||||
|
parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => {
|
||||||
|
let args = if include-required {
|
||||||
|
let values = args.pos()
|
||||||
|
arguments(values, ..args.named())
|
||||||
|
} else if args.pos() == () {
|
||||||
|
args
|
||||||
|
} else {
|
||||||
|
return (false, "element 'diagram': unexpected positional arguments\n hint: these can only be passed to the constructor")
|
||||||
|
}
|
||||||
|
|
||||||
|
default-parser(args, include-required: include-required)
|
||||||
|
},
|
||||||
|
)
|
||||||
94
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/errorbar.typ
vendored
Normal file
94
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/errorbar.typ
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
|
||||||
|
/// An error bar object for a plot. This type allows for quick configuration and
|
||||||
|
/// complete restyling of error bars. For drawing plots with error bars,
|
||||||
|
/// use @plot.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// (1, 2, 3, 4),
|
||||||
|
/// (1, 2, 1.5, 2.1),
|
||||||
|
/// xerr: .2,
|
||||||
|
/// yerr: (.2, .1, .2, .2),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// The default styling can be changed through set rules
|
||||||
|
/// ```example
|
||||||
|
/// #show: lq.set-errorbar(stroke: 1pt + red, cap: none)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// (1, 2, 3, 4),
|
||||||
|
/// (1, 2, 1.5, 2.1),
|
||||||
|
/// xerr: .2,
|
||||||
|
/// yerr: (.2, .1, .2, .2),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let errorbar(
|
||||||
|
|
||||||
|
/// The kind of error bar: horizontal (`"x"`) or vertical (`"y"`).
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
/// The length of the cap. If set to `none`, no cap is drawn.
|
||||||
|
/// -> none | length
|
||||||
|
cap: 3pt,
|
||||||
|
|
||||||
|
/// How to stroke the error bar. If set to `auto`, the stroke is inherited
|
||||||
|
/// the plot.
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// How to stroke the cap. If set to `auto`, the stroke is inherited
|
||||||
|
/// from @errorbar.stroke.
|
||||||
|
/// -> auto | stroke
|
||||||
|
cap-stroke: auto,
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let errorbar = e.element.declare(
|
||||||
|
"errorbar",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
set curve(stroke: it.stroke) if it.stroke != auto
|
||||||
|
|
||||||
|
if it.kind == "x" {
|
||||||
|
place(horizon, curve(curve.line((100%, 0pt))))
|
||||||
|
} else {
|
||||||
|
place(center + horizon, curve(curve.line((0pt, 100%))))
|
||||||
|
}
|
||||||
|
|
||||||
|
if it.cap != none {
|
||||||
|
set curve(stroke: it.cap-stroke) if it.cap-stroke != auto
|
||||||
|
|
||||||
|
if it.kind == "x" {
|
||||||
|
let cap = curve(curve.line((0pt, it.cap)))
|
||||||
|
place(left + horizon, cap)
|
||||||
|
place(right + horizon, cap)
|
||||||
|
} else {
|
||||||
|
let cap = curve(curve.line((it.cap, 0pt)))
|
||||||
|
place(center + top, cap)
|
||||||
|
place(center + bottom, cap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
labelable: false,
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("kind", str, default: "x"),
|
||||||
|
e.field("stroke", e.types.smart(stroke), default: auto),
|
||||||
|
e.field("cap", e.types.option(length), default: 2.5pt),
|
||||||
|
e.field("cap-stroke", e.types.smart(stroke), default: auto),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
113
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/grid.typ
vendored
Normal file
113
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/grid.typ
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
|
||||||
|
/// An axis grid for highlighting tick positions in the diagram area. The
|
||||||
|
/// grid lines are determined from the ticks located by the tick locators
|
||||||
|
/// of the main $x$ and $y$ axes for vertical and horizontal grid lines,
|
||||||
|
/// respectively.
|
||||||
|
///
|
||||||
|
/// One way to set up the grid stroke is with a style rule. The stroke
|
||||||
|
/// for the grid lines at the main ticks is controlled by @grid.stroke and the stroke of the subticks by @grid.stroke-sub:
|
||||||
|
/// ```example
|
||||||
|
/// #show: lq.set-grid(
|
||||||
|
/// stroke: teal,
|
||||||
|
/// stroke-sub: 0.5pt + luma(90%)
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
///
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Through the parameter @diagram.grid, the look of the grid can also be
|
||||||
|
/// controlled directly for an individual diagram.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// grid: (stroke: black, stroke-sub: 0.25pt)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// Here you can also pass `none` to deactivate the grid entirely (equivalent
|
||||||
|
/// to `#show: lq.set-grid(stroke: none)`).
|
||||||
|
///
|
||||||
|
/// In order to address the $x$ and $y$ grid individually, use `cond-set`
|
||||||
|
/// ```example
|
||||||
|
/// #show: lq.cond-set(lq.grid.with(kind: "x"), stroke: orange)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 4.5cm, height: 3cm
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let grid(
|
||||||
|
|
||||||
|
/// A list of tick positions as absolute length coordinates within the
|
||||||
|
/// diagram frame. This is automatically filled by @diagram with the ticks
|
||||||
|
/// resulting from the axes' tick locators.
|
||||||
|
/// -> array
|
||||||
|
ticks,
|
||||||
|
|
||||||
|
/// Whether the ticks passed to @grid.ticks are subticks.
|
||||||
|
/// -> bool
|
||||||
|
sub,
|
||||||
|
|
||||||
|
/// The axis kind: horizontal (`"x"`) or vertical (`"y"`).
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
/// How to stroke grid lines.
|
||||||
|
/// -> none | stroke
|
||||||
|
stroke: 0.5pt + luma(80%),
|
||||||
|
|
||||||
|
/// How to stroke grid lines for subticks. If `auto`, the style is inherited
|
||||||
|
/// from @grid.stroke.
|
||||||
|
/// -> auto | none | stroke
|
||||||
|
stroke-sub: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of the grid in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 0,
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
// A stroke type that can also be auto or none and that still
|
||||||
|
// folds correctly. During folding auto and none override an
|
||||||
|
// existing stroke fully.
|
||||||
|
#let auto-none-stroke = e.types.wrap(
|
||||||
|
e.types.smart(e.types.option(stroke)),
|
||||||
|
fold: old-fold => (outer, inner) => if inner in (none, auto) or outer in (none, auto) { inner } else { (e.types.native.stroke_.fold)(outer, inner) }
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#let grid = e.element.declare(
|
||||||
|
"grid",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
|
||||||
|
let stroke = if it.sub { it.stroke-sub } else { it.stroke }
|
||||||
|
if stroke == none { return }
|
||||||
|
|
||||||
|
let line
|
||||||
|
if it.kind == "x" {
|
||||||
|
line = tick => place(
|
||||||
|
std.line(start: (tick, 0%), end: (tick, 100%), stroke: stroke)
|
||||||
|
)
|
||||||
|
} else if it.kind == "y" {
|
||||||
|
line = tick => place(
|
||||||
|
std.line(start: (0%, tick), end: (100%, tick), stroke: stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
it.ticks.map(line).join()
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("ticks", array, required: true),
|
||||||
|
e.field("sub", bool, default: false),
|
||||||
|
e.field("kind", str, default: "x"),
|
||||||
|
e.field("stroke", e.types.option(stroke), default: 0.5pt + luma(80%)),
|
||||||
|
e.field("stroke-sub", auto-none-stroke, default: none),
|
||||||
|
e.field("z-index", float, default: 0),
|
||||||
|
),
|
||||||
|
)
|
||||||
79
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/label.typ
vendored
Normal file
79
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/label.typ
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
|
||||||
|
|
||||||
|
/// A label for a diagram axis.
|
||||||
|
#let label(
|
||||||
|
|
||||||
|
/// Content to show in the label.
|
||||||
|
/// -> any
|
||||||
|
body,
|
||||||
|
|
||||||
|
/// The kind of axis that the label is attached to.
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
/// Horizontal offset.
|
||||||
|
/// -> length
|
||||||
|
dx: 0pt,
|
||||||
|
|
||||||
|
/// Vertical offset.
|
||||||
|
/// -> length
|
||||||
|
dy: 0pt,
|
||||||
|
|
||||||
|
/// Padding between the axis (and its ticks and labels) and the label.
|
||||||
|
/// When this is set to `none`, the label is drawn directly on the axis
|
||||||
|
/// (ignoring the ticks).
|
||||||
|
/// -> none | length
|
||||||
|
pad: 0.75em,
|
||||||
|
|
||||||
|
/// Angle at which the label is drawn. The label of a `y` axis is often
|
||||||
|
/// drawn at `-90deg`.
|
||||||
|
/// -> angle
|
||||||
|
angle: 0deg
|
||||||
|
|
||||||
|
) = {
|
||||||
|
(
|
||||||
|
body: body,
|
||||||
|
dx: dx,
|
||||||
|
dy: dy,
|
||||||
|
pad: pad,
|
||||||
|
angle: angle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
#let label = e.element.declare(
|
||||||
|
"label",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
let angle = it.angle
|
||||||
|
if it.angle == auto {
|
||||||
|
angle = if it.kind == "y" { -90deg } else { 0deg }
|
||||||
|
}
|
||||||
|
rotate(angle, it.body, reflow: true)
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("body", e.types.option(content), required: true),
|
||||||
|
e.field("dx", relative, default: 0pt),
|
||||||
|
e.field("dy", relative, default: 0pt),
|
||||||
|
e.field(
|
||||||
|
"kind",
|
||||||
|
e.types.union(
|
||||||
|
e.types.literal("x"),
|
||||||
|
e.types.literal("y")
|
||||||
|
),
|
||||||
|
default: "x"
|
||||||
|
),
|
||||||
|
e.field("pad", e.types.option(length), default: .7em),
|
||||||
|
e.field("angle", e.types.smart(angle), default: auto),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let xlabel = label.with(kind: "x")
|
||||||
|
#let ylabel = label.with(kind: "y")
|
||||||
172
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/legend.typ
vendored
Normal file
172
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/legend.typ
vendored
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
#import "../process-styles.typ": twod-ify-alignment
|
||||||
|
#import "../bounds.typ": place-with-bounds
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
/// A diagram legend listing all labeled plots.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// legend: (position: top + left),
|
||||||
|
///
|
||||||
|
/// lq.plot((1,2,3), (1,2,3), label: [Data A]),
|
||||||
|
/// lq.plot((1,2,3), (2,3,4), label: [Data B]),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Also refer to the
|
||||||
|
/// #link("tutorials/legend")[legend tutorial] for more details.
|
||||||
|
#let legend(
|
||||||
|
|
||||||
|
/// The items to place in the legend. This field is
|
||||||
|
/// filled automatically by @diagram.
|
||||||
|
/// -> array
|
||||||
|
..children,
|
||||||
|
|
||||||
|
/// How to fill the background of the legend.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: white.transparentize(20%),
|
||||||
|
|
||||||
|
/// Determines the padding of the entire legend within its box.
|
||||||
|
/// -> relative
|
||||||
|
inset: 0.3em,
|
||||||
|
|
||||||
|
/// How to stroke the outer border of the legend.
|
||||||
|
/// -> none | stroke
|
||||||
|
stroke: 0.5pt + gray,
|
||||||
|
|
||||||
|
/// The radius of the outer border of the legend.
|
||||||
|
/// -> relative | dictionary
|
||||||
|
radius: 1.5pt,
|
||||||
|
|
||||||
|
/// Where to place the legend in the diagram. This can be an alignment
|
||||||
|
/// or a position `(x, y)` where `x` and `y` are relative lengths, i.e.,
|
||||||
|
/// they can be
|
||||||
|
/// - lengths like `20pt` or `2em`,
|
||||||
|
/// - ratios like `50%` (measuring in the data area),
|
||||||
|
/// - or a combination thereof.
|
||||||
|
///
|
||||||
|
/// To place the legend outside the diagram, say to the right, use a
|
||||||
|
/// combination of `position: left + horizon` and `dx: 100%`. Positioning
|
||||||
|
/// of the legend is treated more in-depth in the
|
||||||
|
/// #link("tutorials/legend#positioning")[legend tutorial].
|
||||||
|
/// -> alignment | array
|
||||||
|
position: top + right,
|
||||||
|
|
||||||
|
/// In the case that @legend.position is an `alignment`, `pad` determines
|
||||||
|
/// how much to pad the legend from the outer edge of the data area
|
||||||
|
/// of the diagram.
|
||||||
|
/// -> length
|
||||||
|
pad: 2pt,
|
||||||
|
|
||||||
|
/// The horizontal displacement of the legend from the position specified
|
||||||
|
/// through @legend.position.
|
||||||
|
/// -> relative
|
||||||
|
dx: 0pt,
|
||||||
|
|
||||||
|
/// The vertical displacement of the legend from the position specified
|
||||||
|
/// through @legend.position.
|
||||||
|
/// -> relative
|
||||||
|
dy: 0pt,
|
||||||
|
|
||||||
|
/// Specifies the $z$ position of the legend in the order of rendered
|
||||||
|
/// diagram objects.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 25,
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
#let legend = e.element.declare(
|
||||||
|
"legend",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => box(
|
||||||
|
stroke: it.stroke,
|
||||||
|
inset: it.inset,
|
||||||
|
fill: it.fill,
|
||||||
|
radius: it.radius,
|
||||||
|
grid(..it.children)
|
||||||
|
),
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("children", e.types.array(e.types.any), required: true),
|
||||||
|
e.field("fill", e.types.option(e.types.paint), default: white.transparentize(20%)),
|
||||||
|
e.field("inset", relative, default: 0.3em),
|
||||||
|
e.field("stroke", e.types.option(stroke), default: 0.5pt + gray),
|
||||||
|
e.field("radius", e.types.union(relative, dictionary), default: 1.5pt),
|
||||||
|
|
||||||
|
e.field("position", e.types.union(alignment, array), default: top + right),
|
||||||
|
e.field("pad", length, default: 2pt),
|
||||||
|
e.field("dx", relative, default: 0pt),
|
||||||
|
e.field("dy", relative, default: 0pt),
|
||||||
|
e.field("z-index", float, default: 6),
|
||||||
|
),
|
||||||
|
|
||||||
|
parse-args: (default-parser, fields: none, typecheck: none) => (args, include-required: false) => {
|
||||||
|
let args = if include-required {
|
||||||
|
let values = args.pos()
|
||||||
|
arguments(values, ..args.named())
|
||||||
|
} else if args.pos() == () {
|
||||||
|
args
|
||||||
|
} else {
|
||||||
|
return(false, "element 'legend': unexpected positional arguments\n hint: these can only be passed to the constructor")
|
||||||
|
}
|
||||||
|
|
||||||
|
default-parser(args, include-required: include-required)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#let _place-legend-with-bounds(
|
||||||
|
|
||||||
|
/// -> none | dictionary | legend
|
||||||
|
my-legend,
|
||||||
|
|
||||||
|
/// -> array(array)
|
||||||
|
legend-entries,
|
||||||
|
|
||||||
|
e-get
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let get-settable-field(element, object, field) = {
|
||||||
|
e.fields(object).at(field, default: e-get(element).at(field))
|
||||||
|
}
|
||||||
|
|
||||||
|
if e.eid(my-legend) != e.eid(legend) {
|
||||||
|
my-legend = legend(..legend-entries.join(), ..my-legend)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = get-settable-field(legend, my-legend, "position")
|
||||||
|
let dx = get-settable-field(legend, my-legend, "dx")
|
||||||
|
let dy = get-settable-field(legend, my-legend, "dy")
|
||||||
|
let pad = get-settable-field(legend, my-legend, "pad")
|
||||||
|
|
||||||
|
let alignment = top + left
|
||||||
|
let content-alignment = "inside"
|
||||||
|
|
||||||
|
if type(pos) == std.alignment {
|
||||||
|
alignment = pos
|
||||||
|
} else if type(pos) == array {
|
||||||
|
assert.eq(pos.len(), 2, message: "`legend.position` needs to be a pair of coordinates, got " + repr(pos))
|
||||||
|
dx += pos.at(0)
|
||||||
|
dy += pos.at(1)
|
||||||
|
pad = 0pt
|
||||||
|
}
|
||||||
|
|
||||||
|
place-with-bounds(
|
||||||
|
alignment: alignment,
|
||||||
|
content-alignment: content-alignment,
|
||||||
|
dx: dx, dy: dy, pad: pad,
|
||||||
|
wrap-in-box: true,
|
||||||
|
{
|
||||||
|
set grid(
|
||||||
|
columns: 2,
|
||||||
|
stroke: none,
|
||||||
|
inset: 2pt,
|
||||||
|
align: horizon + start
|
||||||
|
)
|
||||||
|
set grid.cell(breakable: false)
|
||||||
|
my-legend
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
190
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/mark.typ
vendored
Normal file
190
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/mark.typ
vendored
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
#import "@preview/tiptoe:0.3.1": arc
|
||||||
|
|
||||||
|
|
||||||
|
/// A mark for a plot. Refer to the #link("tutorials/marks")[mark tutorial] for
|
||||||
|
/// a list of available mark shapes and more details.
|
||||||
|
#let mark(
|
||||||
|
|
||||||
|
/// The size of the mark. The built-in mark shapes are tuned to match in
|
||||||
|
/// optical size, see #link("tutorials/marks#sizing")[mark sizing].
|
||||||
|
/// -> length
|
||||||
|
size: 4pt,
|
||||||
|
|
||||||
|
/// How to fill the mark. If set to `auto`, the fill is inherited from the
|
||||||
|
/// plot.
|
||||||
|
/// -> auto | none | color | gradient | tiling
|
||||||
|
fill: auto,
|
||||||
|
|
||||||
|
/// How to stroke the mark. If set to `auto`, the stroke is inherited from the
|
||||||
|
/// plot.
|
||||||
|
/// -> stroke
|
||||||
|
stroke: 0.7pt,
|
||||||
|
|
||||||
|
/// The shape of the mark. This can be a string identifying one of the
|
||||||
|
/// built-in marks (check out the tutorial) or a function that takes a mark
|
||||||
|
/// and produces content.
|
||||||
|
/// -> str | function
|
||||||
|
shape: "."
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
#let mark = grid
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let circle = mark => {
|
||||||
|
let radius = mark.size / 2
|
||||||
|
move(
|
||||||
|
dx: -radius,
|
||||||
|
dy: -radius,
|
||||||
|
std.ellipse(width: radius*2, height: radius*2, fill: mark.fill, stroke: mark.stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let small-circle = mark => {
|
||||||
|
let radius = mark.size / 4
|
||||||
|
move(
|
||||||
|
dx: -radius,
|
||||||
|
dy: -radius,
|
||||||
|
std.ellipse(width: radius*2, height: radius*2, fill: mark.fill, stroke: mark.stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let point = mark => {
|
||||||
|
let radius = .5pt
|
||||||
|
move(
|
||||||
|
dx: -radius,
|
||||||
|
dy: -radius,
|
||||||
|
std.circle(radius: radius, fill: mark.fill, stroke: mark.stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let square = mark => {
|
||||||
|
let s = mark.size * 0.85
|
||||||
|
move(
|
||||||
|
dx: -s / 2,
|
||||||
|
dy: -s / 2,
|
||||||
|
rect(width: s, height: s, fill: mark.fill, stroke: mark.stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let cross = mark => {
|
||||||
|
let s = mark.size / calc.sqrt(8) * 1.2
|
||||||
|
place(line(start: (-s, -s), end: (s, s), stroke: mark.stroke))
|
||||||
|
line(start: (s, -s), end: (-s, s), stroke: mark.stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// #let plus = mark => {
|
||||||
|
// let s = mark.size / 2 * 1.2
|
||||||
|
// place(line(start: (0pt, -s), end: (0pt, s), stroke: mark.stroke))
|
||||||
|
// line(start: (s, 0pt), end: (-s, 0pt), stroke: mark.stroke)
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
#let polygon = (mark, n: 5, angle: 0deg) => {
|
||||||
|
// The last term serves for equalizing the apparent size of polygons with different n.
|
||||||
|
let radius = mark.size / 2 * calc.sqrt(1 + 4 / (n*n))
|
||||||
|
let dy = (0, .13, 0, .03, 0, .02).at(n - 2, default: 0) * radius
|
||||||
|
let poly = std.polygon(
|
||||||
|
stroke: mark.stroke, fill: mark.fill,
|
||||||
|
..range(n).map(i => {
|
||||||
|
let phi = i * 360deg / n
|
||||||
|
(
|
||||||
|
radius * calc.sin(phi),
|
||||||
|
-radius * calc.cos(phi) + dy
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
if angle != 0deg {
|
||||||
|
poly = rotate(angle, origin: left + top, poly)
|
||||||
|
}
|
||||||
|
poly
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let star = (mark, n: 5, angle: 0deg, inset: 60%) => {
|
||||||
|
let radius = mark.size / 2 * 1.15
|
||||||
|
|
||||||
|
std.polygon(
|
||||||
|
stroke: mark.stroke,
|
||||||
|
fill: mark.fill,
|
||||||
|
..range(n * 2).map(i => {
|
||||||
|
let r = if calc.even(i) { radius } else { radius * (100% - inset) }
|
||||||
|
let phi = i * 360deg / n / 2 + angle
|
||||||
|
(
|
||||||
|
r * calc.sin(phi),
|
||||||
|
-r * calc.cos(phi)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let asterisk = star.with(inset: 100%)
|
||||||
|
|
||||||
|
|
||||||
|
#let moon = (mark, angle: 0deg) => {
|
||||||
|
let radius = mark.size / 2
|
||||||
|
|
||||||
|
arc(
|
||||||
|
radius: radius,
|
||||||
|
fill: mark.fill,
|
||||||
|
closed: "segment",
|
||||||
|
arc: 180deg,
|
||||||
|
stroke: 0pt,
|
||||||
|
angle: angle
|
||||||
|
)
|
||||||
|
move(
|
||||||
|
dx: -radius,
|
||||||
|
dy: -radius,
|
||||||
|
std.circle(radius: radius, fill: none, stroke: mark.stroke)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let text-mark = (mark, body: emoji.heart) => place(
|
||||||
|
center + horizon,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let marks = (
|
||||||
|
".": small-circle,
|
||||||
|
",": point,
|
||||||
|
"*": asterisk,
|
||||||
|
"asterisk": asterisk,
|
||||||
|
"x": cross,
|
||||||
|
"+": asterisk.with(n: 4),
|
||||||
|
"|": polygon.with(n: 2),
|
||||||
|
"-": polygon.with(n: 2, angle: 90deg),
|
||||||
|
"a3": asterisk.with(n: 3),
|
||||||
|
"a4": asterisk.with(n: 4),
|
||||||
|
"a5": asterisk.with(n: 5),
|
||||||
|
"a6": asterisk.with(n: 6),
|
||||||
|
"<": polygon.with(n: 3, angle: -90deg),
|
||||||
|
">": polygon.with(n: 3, angle: 90deg),
|
||||||
|
"^": polygon.with(n: 3),
|
||||||
|
"v": polygon.with(n: 3, angle: 180deg),
|
||||||
|
"o": circle,
|
||||||
|
"s": square,
|
||||||
|
"polygon": polygon,
|
||||||
|
"d": polygon.with(n: 4),
|
||||||
|
"p5": polygon.with(n: 5),
|
||||||
|
"p6": polygon.with(n: 6),
|
||||||
|
"p7": polygon.with(n: 7),
|
||||||
|
"p8": polygon.with(n: 8),
|
||||||
|
"star": star,
|
||||||
|
"s3": star.with(n: 3, inset: 70%),
|
||||||
|
"s4": star.with(n: 4),
|
||||||
|
"s5": star.with(n: 5),
|
||||||
|
"s6": star.with(n: 6),
|
||||||
|
"moon": moon,
|
||||||
|
"text": text-mark,
|
||||||
|
"none": mark => none
|
||||||
|
)
|
||||||
52
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/spine.typ
vendored
Normal file
52
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/spine.typ
vendored
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
#import "@preview/tiptoe:0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
/// The spine of a diagram axis (the line drawn along the axis).
|
||||||
|
#let spine(
|
||||||
|
|
||||||
|
/// How to stroke the spine of the axis.
|
||||||
|
/// -> none | stroke
|
||||||
|
stroke: (thickness: 0.5pt, cap: "square"),
|
||||||
|
|
||||||
|
/// Places an arrow tip on the axis spine. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
tip: none,
|
||||||
|
|
||||||
|
/// Places an arrow tail on the axis spine. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
toe: none
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let spine = e.element.declare(
|
||||||
|
"spine",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
if it.stroke == none { return }
|
||||||
|
let line
|
||||||
|
if it.kind == "x" {
|
||||||
|
line = tiptoe.line.with(tip: it.tip, toe: it.toe)
|
||||||
|
} else if it.kind == "y"{
|
||||||
|
line = tiptoe.line.with(angle: 90deg, tip: it.toe, toe: it.tip)
|
||||||
|
}
|
||||||
|
line(length: 100%, stroke: it.stroke)
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("stroke", e.types.option(stroke), default: (thickness: 0.5pt, cap: "square")),
|
||||||
|
e.field(
|
||||||
|
"kind",
|
||||||
|
e.types.union(e.types.literal("x"), e.types.literal("y")),
|
||||||
|
default: "x"
|
||||||
|
),
|
||||||
|
e.field("tip", e.types.option(function), default: none),
|
||||||
|
e.field("toe", e.types.option(function), default: none),
|
||||||
|
)
|
||||||
|
)
|
||||||
206
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/tick.typ
vendored
Normal file
206
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/tick.typ
vendored
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
|
||||||
|
#import "../process-styles.typ": twod-ify-alignment
|
||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
#import "spine.typ": spine
|
||||||
|
|
||||||
|
|
||||||
|
/// A tick label, usually a number denoting the coordinate value.
|
||||||
|
/// This type exists prominently to enable `show` rules on tick labels.
|
||||||
|
#let tick-label(
|
||||||
|
|
||||||
|
/// Content to show in the label.
|
||||||
|
/// -> content
|
||||||
|
body,
|
||||||
|
|
||||||
|
/// Whether this is a label for a subtick.
|
||||||
|
/// -> bool
|
||||||
|
sub: false,
|
||||||
|
|
||||||
|
/// The kind of axis that the tick label is attached to.
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let tick-label = e.element.declare(
|
||||||
|
"tick-label",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => {
|
||||||
|
it.body
|
||||||
|
},
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("body", e.types.option(content), required: true),
|
||||||
|
e.field(
|
||||||
|
"kind",
|
||||||
|
e.types.union(e.types.literal("x"), e.types.literal("y")),
|
||||||
|
default: "x"
|
||||||
|
),
|
||||||
|
e.field("sub", bool, default: false),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// A tick or subtick on a diagram axis. A tick consists of a tick mark on the axis and
|
||||||
|
/// a tick label, usually a number denoting the coordinate value.
|
||||||
|
#let tick(
|
||||||
|
|
||||||
|
/// Position of the tick in data coordinates.
|
||||||
|
/// -> float
|
||||||
|
value,
|
||||||
|
|
||||||
|
/// The label for the tick.
|
||||||
|
/// -> content | lq.tick-label
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether this is a subtick.
|
||||||
|
/// -> bool
|
||||||
|
sub: false,
|
||||||
|
|
||||||
|
/// The kind of axis that the tick is attached to.
|
||||||
|
/// -> "x" | "y"
|
||||||
|
kind: "x",
|
||||||
|
|
||||||
|
/// How to stroke the tick mark. If set to `auto`, the stroke is inherited
|
||||||
|
/// the axis spine.
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// How much to shorten sub ticks compared to regular ticks.
|
||||||
|
/// -> ratio
|
||||||
|
shorten-sub: 50%,
|
||||||
|
|
||||||
|
/// Where to align the tick. For example, if set to `right`, the tick label is
|
||||||
|
/// shown to the left of the tick, aligning at its right side. For example,
|
||||||
|
/// ticks on a $y$-axis on the left side of the diagram will typically be
|
||||||
|
/// aligned on the right.
|
||||||
|
/// -> left | top | right | bottom
|
||||||
|
align: right,
|
||||||
|
|
||||||
|
/// The padding to add between the tick and the tick label.
|
||||||
|
/// -> length
|
||||||
|
pad: 0.5em,
|
||||||
|
|
||||||
|
/// The length of the tick on the inside of the axis. Here, the tick label
|
||||||
|
/// is considered to be on the _outside_. For example, if @tick.align is set
|
||||||
|
/// to `right`, the inset determines the tick length to the right of the
|
||||||
|
/// axis spine.
|
||||||
|
/// -> length
|
||||||
|
inset: 4pt,
|
||||||
|
|
||||||
|
/// Length of the tick on the outside of the axis, see @tick.inset. For
|
||||||
|
/// example, if @tick.align is set to `right`, the inset determines the tick
|
||||||
|
/// length to the left of the axis spine.
|
||||||
|
/// -> length
|
||||||
|
outset: 0pt
|
||||||
|
|
||||||
|
) = {}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let tick = e.element.declare(
|
||||||
|
"tick",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => e.get(e-get => {
|
||||||
|
let angle = if it.align in (top, bottom) { 90deg } else { 0deg }
|
||||||
|
let factor = if it.sub { 1 - (it.shorten-sub / 100%) } else { 1 }
|
||||||
|
let outset = it.outset * factor
|
||||||
|
let length = (it.inset + it.outset) * factor
|
||||||
|
|
||||||
|
let stroke = it.stroke
|
||||||
|
if stroke == auto {
|
||||||
|
stroke = e-get(spine).stroke
|
||||||
|
}
|
||||||
|
|
||||||
|
let label = it.label
|
||||||
|
if e.eid(label) != e.eid(tick-label) {
|
||||||
|
label = tick-label(label, sub: it.sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
let tline = line(length: length, angle: angle, stroke: stroke)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if it.align == right {
|
||||||
|
move(dx: -outset, {
|
||||||
|
tline + place(dx: -length - it.pad, right + horizon, label)
|
||||||
|
})
|
||||||
|
} else if it.align == left {
|
||||||
|
move(dx: -length + outset, {
|
||||||
|
tline + place(dx: length + it.pad, left + horizon, label)
|
||||||
|
})
|
||||||
|
} else if it.align == top {
|
||||||
|
move(dy: -length + outset, {
|
||||||
|
tline + place(dy: length + it.pad, top + center, label)
|
||||||
|
});
|
||||||
|
} else if it.align == bottom {
|
||||||
|
move(dy: -outset, {
|
||||||
|
tline + place(dy: -length - it.pad, bottom + center, label)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
labelable: false,
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("value", float, required: true),
|
||||||
|
e.field("sub", bool, default: false),
|
||||||
|
e.field(
|
||||||
|
"kind",
|
||||||
|
e.types.union(e.types.literal("x"), e.types.literal("y")),
|
||||||
|
default: "x"
|
||||||
|
),
|
||||||
|
e.field("label", e.types.any, default: none),
|
||||||
|
e.field("align", e.types.wrap(alignment, fold: none), default: right),
|
||||||
|
e.field("stroke", e.types.smart(stroke), default: auto),
|
||||||
|
e.field("shorten-sub", ratio, default: 50%),
|
||||||
|
e.field("pad", length, default: 0.5em),
|
||||||
|
e.field("inset", length, default: 3pt),
|
||||||
|
e.field("outset", length, default: 0pt),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
#box(
|
||||||
|
stroke: red,
|
||||||
|
width: 1cm, height: 1cm,
|
||||||
|
tick(34, label: [304])
|
||||||
|
)
|
||||||
|
|
||||||
|
#box(
|
||||||
|
stroke: red,
|
||||||
|
width: 1cm, height: 1cm,
|
||||||
|
tick(34, label: [304], align: left)
|
||||||
|
)
|
||||||
|
|
||||||
|
#box(
|
||||||
|
stroke: red,
|
||||||
|
width: 1cm, height: 1cm,
|
||||||
|
tick(34, label: [304], align: top)
|
||||||
|
)
|
||||||
|
#box(
|
||||||
|
stroke: red,
|
||||||
|
width: 1cm, height: 1cm,
|
||||||
|
tick(34, label: [304], align: bottom)
|
||||||
|
)
|
||||||
|
|
||||||
|
\
|
||||||
|
\
|
||||||
|
|
||||||
|
// #tick(34, label: [34])
|
||||||
|
// #tick(34, label: [34], align: left)
|
||||||
|
// #tick(34, label: [34], align: top)
|
||||||
|
// #tick(34, label: [34], align: bottom)
|
||||||
|
|
||||||
|
// #tick(34, label: [34], inset: 2pt)
|
||||||
|
// #tick(34, label: [34], align: left, inset: 2pt)
|
||||||
|
// #tick(34, label: [34], align: top, inset: 2pt)
|
||||||
|
// #tick(34, label: [34], align: bottom, inset: 2pt)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
94
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/title.typ
vendored
Normal file
94
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/model/title.typ
vendored
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
#import "@preview/elembic:1.1.1" as e
|
||||||
|
|
||||||
|
|
||||||
|
/// A title for a diagram. Titles can be placed at the top (default),
|
||||||
|
/// left, right, or bottom of a diagram.
|
||||||
|
#let title(
|
||||||
|
|
||||||
|
/// The content to show in the title.
|
||||||
|
/// -> any
|
||||||
|
body,
|
||||||
|
|
||||||
|
/// Position of the title in the diagram.
|
||||||
|
/// -> left | top | right | bottom
|
||||||
|
position: top,
|
||||||
|
|
||||||
|
/// Horizontal offset.
|
||||||
|
/// -> length
|
||||||
|
dx: 0pt,
|
||||||
|
|
||||||
|
/// Vertical offset.
|
||||||
|
/// -> length
|
||||||
|
dy: 0pt,
|
||||||
|
|
||||||
|
/// Padding between the axes and the title.
|
||||||
|
/// -> length
|
||||||
|
pad: 0.5em,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assert(
|
||||||
|
position in (top, bottom, left, right),
|
||||||
|
message: "`position` needs to be one of \"top\", \"left\", \"bottom\", and \"right\""
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
body: body,
|
||||||
|
position: position,
|
||||||
|
dx: dx,
|
||||||
|
dy: dy,
|
||||||
|
pad: pad
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let title = e.element.declare(
|
||||||
|
"title",
|
||||||
|
prefix: "lilaq",
|
||||||
|
|
||||||
|
display: it => it.body,
|
||||||
|
|
||||||
|
fields: (
|
||||||
|
e.field("body", content, required: true),
|
||||||
|
e.field("position", alignment, default: top),
|
||||||
|
e.field("dx", length, default: 0pt),
|
||||||
|
e.field("dy", length, default: 0pt),
|
||||||
|
e.field("pad", length, default: 0.5em),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#import "../bounds.typ": place-with-bounds
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let lq-title = title
|
||||||
|
|
||||||
|
#let _place-title-with-bounds(
|
||||||
|
title,
|
||||||
|
get-settable-field,
|
||||||
|
width,
|
||||||
|
height
|
||||||
|
) = {
|
||||||
|
|
||||||
|
if e.eid(title) != e.eid(lq-title) {
|
||||||
|
title = lq-title(title)
|
||||||
|
}
|
||||||
|
|
||||||
|
let position = get-settable-field(lq-title, title, "position")
|
||||||
|
let dx = get-settable-field(lq-title, title, "dx")
|
||||||
|
let dy = get-settable-field(lq-title, title, "dy")
|
||||||
|
let pad = get-settable-field(lq-title, title, "pad")
|
||||||
|
|
||||||
|
let wrapper = if position in (top, bottom) {
|
||||||
|
box.with(width: width)
|
||||||
|
} else if position in (left, right) {
|
||||||
|
box.with(height: height)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
place-with-bounds(
|
||||||
|
wrapper(title), alignment: position, dx: dx, dy: dy, pad: pad
|
||||||
|
)
|
||||||
|
}
|
||||||
15
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/place-anchor.typ
vendored
Normal file
15
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/place-anchor.typ
vendored
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
|
||||||
|
#let place-anchor(x, y, name) = {
|
||||||
|
(
|
||||||
|
label: none,
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
let (x, y) = transform(x, y)
|
||||||
|
place(
|
||||||
|
dx: x, dy: y, [#box()#label(name)]
|
||||||
|
)
|
||||||
|
},
|
||||||
|
xlimits: () => none,
|
||||||
|
ylimits: () => none,
|
||||||
|
legend-handle: (..) => none
|
||||||
|
)
|
||||||
|
}
|
||||||
314
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/bar.typ
vendored
Normal file
314
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/bar.typ
vendored
Normal file
@@ -0,0 +1,314 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": bar-lim
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../process-styles.typ": merge-fills
|
||||||
|
#import "../utility.typ": match-type, match
|
||||||
|
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points, stepify
|
||||||
|
#import "../math.typ": vec, minmax
|
||||||
|
#import "../style/styling.typ": prepare-path
|
||||||
|
|
||||||
|
|
||||||
|
#let render-bar(
|
||||||
|
plot,
|
||||||
|
transform,
|
||||||
|
orientation: "vertical"
|
||||||
|
) = {
|
||||||
|
|
||||||
|
let offset-coeff = plot.offset-coeff
|
||||||
|
|
||||||
|
let get-bar-range = match-type(
|
||||||
|
plot.width,
|
||||||
|
int: () => i => (
|
||||||
|
plot.width * offset-coeff, plot.width * (1 + offset-coeff),
|
||||||
|
),
|
||||||
|
float: () => i => (
|
||||||
|
plot.width * offset-coeff, plot.width * (1 + offset-coeff),
|
||||||
|
),
|
||||||
|
array: () => i => (
|
||||||
|
plot.width.at(i) * offset-coeff, plot.width.at(i) * (1 + offset-coeff),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
show: prepare-path.with(
|
||||||
|
fill: if type(plot.style.fill) != array { plot.style.fill},
|
||||||
|
stroke: plot.style.stroke,
|
||||||
|
element: rect
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let colored-rect = {
|
||||||
|
if type(plot.style.fill) == array {
|
||||||
|
(i, width: 0pt, height: 0pt) => rect(
|
||||||
|
width: width, height: height, fill: plot.style.fill.at(i)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
(i, width: 0pt, height: 0pt) => rect(width: width, height: height)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
colored-rect(0, width: 100%, height: 100%)
|
||||||
|
} else {
|
||||||
|
|
||||||
|
if orientation == "vertical" {
|
||||||
|
|
||||||
|
for i in range(plot.x.len()) {
|
||||||
|
let y = plot.y.at(i)
|
||||||
|
if float.is-nan(y) { continue }
|
||||||
|
|
||||||
|
let (x1, x2) = get-bar-range(i)
|
||||||
|
let x = plot.x.at(i)
|
||||||
|
|
||||||
|
let (xx1, y0) = transform(x + x1, plot.base.at(i, default: plot.base.first()))
|
||||||
|
let (xx2, yy) = transform(x + x2, y)
|
||||||
|
|
||||||
|
place(dx: xx1, dy: yy, colored-rect(i, width: xx2 - xx1, height: y0 - yy))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if orientation == "horizontal" {
|
||||||
|
|
||||||
|
for i in range(plot.y.len()) {
|
||||||
|
let x = plot.x.at(i)
|
||||||
|
if float.is-nan(x) { continue }
|
||||||
|
|
||||||
|
let (y1, y2) = get-bar-range(i)
|
||||||
|
let y = plot.y.at(i)
|
||||||
|
|
||||||
|
let (x0, yy1) = transform(plot.base.at(i, default: plot.base.first()), y + y1)
|
||||||
|
let (xx, yy2) = transform(x, y + y2)
|
||||||
|
|
||||||
|
place(dx: x0, dy: yy2, colored-rect(i, width: xx - x0, height: yy1 - yy2))
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a bar plot from the given bar positions and heights.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// xaxis: (subticks: none),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1, 2, 3, 4, 5, 6),
|
||||||
|
/// (1, 2, 3, 2, 5, 3),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// The example below demonstrates how to use custom tick labels by passing
|
||||||
|
/// an array of `(location, label)` tuples to @axis.ticks. In addition, we show
|
||||||
|
/// how to rotate the labels by 45° and align them nicely to the ticks.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// xaxis: (
|
||||||
|
/// ticks: ("Apples", "Bananas", "Kiwis", "Mangos", "Papayas")
|
||||||
|
/// .map(rotate.with(-45deg, reflow: true))
|
||||||
|
/// .map(align.with(right))
|
||||||
|
/// .enumerate(),
|
||||||
|
/// subticks: none,
|
||||||
|
/// ),
|
||||||
|
/// lq.bar(
|
||||||
|
/// range(5),
|
||||||
|
/// (5, 3, 4, 2, 1)
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let bar(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates specifying the bar positions.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Specifies the bar heights either through an array of $y$ coordinates or
|
||||||
|
/// a function that takes an `x` value and returns a corresponding `y`
|
||||||
|
/// coordinate. The number of $x$ and $y$ coordinates must match.
|
||||||
|
/// -> array | function
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// How to fill the bars. This can be a single value applied to all bars or
|
||||||
|
/// an array with the same length as the coordinate arrays.
|
||||||
|
/// -> none | color | gradient | tiling | array
|
||||||
|
fill: auto,
|
||||||
|
|
||||||
|
/// How to stroke the bars. All values allowed by the built-in `rect`
|
||||||
|
/// function are also allowed here, see
|
||||||
|
/// [`std.rect#stroke`](https://typst.app/docs/reference/visualize/rect/#parameters-stroke).
|
||||||
|
/// In particular, note that by passing a dictionary, the individual sides
|
||||||
|
/// of the bars can be stroked independently.
|
||||||
|
/// -> auto | none | color | length | stroke | gradient | tiling | dictionary
|
||||||
|
stroke: none,
|
||||||
|
|
||||||
|
/// Alignment of the bars at the $x$ values.
|
||||||
|
/// #details[
|
||||||
|
/// Demonstration of the different alignment modes.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// xaxis: (subticks: none),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1,2,3,4,5), (1,2,3,4,5),
|
||||||
|
/// width: 0.2, fill: red,
|
||||||
|
/// align: left, label: "left"
|
||||||
|
/// ),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1,2,3,4,5), (5,4,3,2,1),
|
||||||
|
/// width: 0.2, fill: blue,
|
||||||
|
/// align: right, label: "right"
|
||||||
|
/// ),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1,2,3,4,5), (2.5,) * 5,
|
||||||
|
/// width: 0.2, fill: rgb("#AAEEAA99"),
|
||||||
|
/// align: center, label: "center"
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> left | center | right
|
||||||
|
align: center,
|
||||||
|
|
||||||
|
/// Width of the bars in data coordinates. The width can be set either to a
|
||||||
|
/// constant for all bars or per-bar by passing an array with
|
||||||
|
/// the same length as the coordinate arrays.
|
||||||
|
/// #details[
|
||||||
|
/// Example for a bar plot with varying bar widths.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1, 2, 3, 4, 5),
|
||||||
|
/// (1, 2, 3, 2, 5),
|
||||||
|
/// width: (1, 0.5, 1, 0.5, 1),
|
||||||
|
/// fill: orange,
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> int | float | array
|
||||||
|
width: 0.8,
|
||||||
|
|
||||||
|
/// An offset to apply to all $x$ coordinates. This is equivalent to replacing
|
||||||
|
/// the array passed to @bar.x with `x.map(x => x + offset)`. Using an offset
|
||||||
|
/// can be useful to avoid overlaps when plotting multiple bar plots into one
|
||||||
|
/// diagram.
|
||||||
|
/// -> int | float | array
|
||||||
|
offset: 0,
|
||||||
|
|
||||||
|
|
||||||
|
/// Defines the $y$ coordinate of the baseline of the bars. This can either
|
||||||
|
/// be a constant value applied to all bars or it can be set per-bar by
|
||||||
|
/// passing an array with the same length as the coordinate arrays.
|
||||||
|
/// #details[
|
||||||
|
/// Bar plot with varying base.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// xaxis: (subticks: none),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1, 2, 3, 4, 5),
|
||||||
|
/// (1, 2, 3, 0, 5),
|
||||||
|
/// base: (0, 1, 2, -1, 0),
|
||||||
|
/// fill: white,
|
||||||
|
/// stroke: 0.7pt
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> int | float | array
|
||||||
|
base: 0,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(y) == function {
|
||||||
|
y = x.map(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(
|
||||||
|
x, y, width: width, base: base, fill: fill, fn-name: "bar"
|
||||||
|
)
|
||||||
|
|
||||||
|
if offset != 0 {
|
||||||
|
if type(offset) == array {
|
||||||
|
assertations.assert-matching-data-dimensions(
|
||||||
|
x, y, offset: offset, fn-name: "bar"
|
||||||
|
)
|
||||||
|
x = x.zip(offset).map(array.sum)
|
||||||
|
} else {
|
||||||
|
x = x.map(x => x + offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(base) != array {
|
||||||
|
base = (base,)
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset-coeff = match(
|
||||||
|
align,
|
||||||
|
left, 0,
|
||||||
|
center, -0.5,
|
||||||
|
right, -1
|
||||||
|
)
|
||||||
|
|
||||||
|
let simple-lims() = vec.add(
|
||||||
|
minmax(x),
|
||||||
|
(offset-coeff*width, (1 + offset-coeff) * width)
|
||||||
|
)
|
||||||
|
|
||||||
|
let xlim = match-type(
|
||||||
|
width,
|
||||||
|
int: simple-lims,
|
||||||
|
float: simple-lims,
|
||||||
|
array: () => (
|
||||||
|
calc.min(..x.zip(width).map(((x, w)) => x + offset-coeff * w)),
|
||||||
|
calc.max(..x.zip(width).map(((x, w)) => x + (1 + offset-coeff) * w)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: width,
|
||||||
|
offset-coeff: offset-coeff,
|
||||||
|
label: label,
|
||||||
|
base: base,
|
||||||
|
style: (
|
||||||
|
align: align,
|
||||||
|
stroke: stroke,
|
||||||
|
fill: fill
|
||||||
|
),
|
||||||
|
plot: render-bar,
|
||||||
|
xlimits: () => xlim,
|
||||||
|
ylimits: () => bar-lim(y, base),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
344
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/boxplot.typ
vendored
Normal file
344
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/boxplot.typ
vendored
Normal file
@@ -0,0 +1,344 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../utility.typ" as utility: match
|
||||||
|
#import "../algorithm/boxplot.typ": *
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
|
||||||
|
#let render-boxplot(plot, transform) = {
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.line(length: 100%, stroke: plot.style.stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let style = plot.style
|
||||||
|
|
||||||
|
|
||||||
|
for (i, statistics) in plot.statistics.enumerate() {
|
||||||
|
|
||||||
|
let x1 = plot.width.at(i) * -0.5
|
||||||
|
let x2 = plot.width.at(i) * 0.5
|
||||||
|
let x = plot.x.at(i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let (xx1, q1) = transform(x + x1, statistics.q1)
|
||||||
|
let (xx2, q3) = transform(x + x2, statistics.q3)
|
||||||
|
let (middle, median) = transform(x, statistics.median)
|
||||||
|
let (_, whisker-low) = transform(x, statistics.whisker-low)
|
||||||
|
let (_, whisker-high) = transform(x, statistics.whisker-high)
|
||||||
|
let (xm1, _) = transform(x - style.cap-length / 2, statistics.q1)
|
||||||
|
let (xm2, _) = transform(x + style.cap-length / 2, statistics.q1)
|
||||||
|
|
||||||
|
|
||||||
|
let box-thickness = utility.if-auto(stroke(style.stroke).thickness, 1pt)
|
||||||
|
// x axis may be inverted:
|
||||||
|
if xx1 > xx2 { box-thickness *= -1 }
|
||||||
|
|
||||||
|
|
||||||
|
place(line(
|
||||||
|
start: (middle, q1),
|
||||||
|
end: (middle, whisker-low),
|
||||||
|
stroke: style.whisker
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (middle, q3),
|
||||||
|
end: (middle, whisker-high),
|
||||||
|
stroke: style.whisker
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (xm1, whisker-low),
|
||||||
|
end: (xm2, whisker-low),
|
||||||
|
stroke: style.cap
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (xm1, whisker-high),
|
||||||
|
end: (xm2, whisker-high),
|
||||||
|
stroke: style.cap
|
||||||
|
))
|
||||||
|
|
||||||
|
place(
|
||||||
|
dx: xx1, dy: q1,
|
||||||
|
rect(
|
||||||
|
height: q3 - q1, width: xx2 - xx1,
|
||||||
|
fill: style.fill, stroke: style.stroke
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
place(line(
|
||||||
|
start: (xx1 + box-thickness / 2, median),
|
||||||
|
end: (xx2 - box-thickness / 2, median),
|
||||||
|
stroke: style.median
|
||||||
|
))
|
||||||
|
|
||||||
|
if style.mean != none {
|
||||||
|
let (_, mean) = transform(x, statistics.mean)
|
||||||
|
|
||||||
|
if type(style.mean) in (str, function) {
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mean,
|
||||||
|
size: plot.style.mark-size,
|
||||||
|
fill: plot.style.outlier-fill
|
||||||
|
)
|
||||||
|
set mark(stroke: plot.style.outlier-stroke)
|
||||||
|
place(dx: middle, dy: mean, mark())
|
||||||
|
|
||||||
|
} else {
|
||||||
|
place(line(
|
||||||
|
start: (xx1 + box-thickness/2, mean),
|
||||||
|
end: (xx2 - box-thickness/2, mean),
|
||||||
|
stroke: style.mean
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if plot.style.mark != none {
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mark,
|
||||||
|
size: plot.style.mark-size,
|
||||||
|
fill: plot.style.outlier-fill
|
||||||
|
)
|
||||||
|
set mark(stroke: plot.style.outlier-stroke)
|
||||||
|
for outlier in statistics.outliers {
|
||||||
|
let (_, y) = transform(x + x1, outlier)
|
||||||
|
place(dx: middle, dy: y, mark())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes and visualizes one or more boxplots from datasets.
|
||||||
|
///
|
||||||
|
/// In the following example, boxplots are generated for four different
|
||||||
|
/// datasets. The default boxplot visualizes the first and third quartil as
|
||||||
|
/// well as the median of the data with a box and shows traditional whiskers
|
||||||
|
/// and outliers.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// stroke: blue.darken(50%),
|
||||||
|
/// (1, 2, 3, 4, 5, 6, 7, 8, 9, 21, 19),
|
||||||
|
/// range(1, 30),
|
||||||
|
/// (1, 28, 25, 30),
|
||||||
|
/// (1, 2, 3, 4, 5, 6, 32),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// By default, the mean value is not shown but it can be visualized by setting
|
||||||
|
/// the @boxplot.mean parameter to a mark or line stroke.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.boxplot((1, 3, 10), mean: "."),
|
||||||
|
/// lq.boxplot((1, 3, 10), mean: green, x: 2),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Boxplots can be richly customized as demonstrated below.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// (1, 3, 10),
|
||||||
|
/// stroke: luma(30%),
|
||||||
|
/// fill: yellow,
|
||||||
|
/// median: red
|
||||||
|
/// ),
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// (1.5, 3, 9),
|
||||||
|
/// x: 2,
|
||||||
|
/// whisker: blue,
|
||||||
|
/// cap: red,
|
||||||
|
/// cap-length: 0.7,
|
||||||
|
/// median: green
|
||||||
|
/// ),
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// lq.linspace(5.3, 6.2) + (2, 3, 7, 9.5),
|
||||||
|
/// x: 3,
|
||||||
|
/// outliers: "x"
|
||||||
|
/// ),
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// lq.linspace(5.3, 6.2) + (2, 3, 7, 9.5),
|
||||||
|
/// x: 4,
|
||||||
|
/// outliers: none
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Some data sets might be too large to be processed in Typst. In this case,
|
||||||
|
/// the median, the first and third quartil as well as the whiskers can be
|
||||||
|
/// computed somewhere else and specified manually in Lilaq (see also
|
||||||
|
/// @boxplot.data).
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 4cm,
|
||||||
|
/// lq.boxplot(
|
||||||
|
/// (
|
||||||
|
/// median: 4.4,
|
||||||
|
/// q1: 2,
|
||||||
|
/// q3: 8,
|
||||||
|
/// outliers: (12, 13),
|
||||||
|
/// whisker-low: 0,
|
||||||
|
/// whisker-high: 10,
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let boxplot(
|
||||||
|
|
||||||
|
/// One or more data sets to generate a boxplot from. A data set can either be
|
||||||
|
/// - an array of values (in this case all statistics are computed automatically) or
|
||||||
|
/// - a dictionary with the mandatory keys `median`, `q1`, `q3`,
|
||||||
|
/// `whisker-low`, and `whisker-high` and optional keys `mean` and
|
||||||
|
/// `outliers`. This method is useful for precomputed distributions,
|
||||||
|
/// especially data sets that are too large for processing in Typst.
|
||||||
|
///
|
||||||
|
/// -> array | dictionary
|
||||||
|
..data,
|
||||||
|
|
||||||
|
/// The $x$ coordinate(s) to draw the boxplots at. If set to `auto`, boxplots will
|
||||||
|
/// be created at integer positions starting with 1.
|
||||||
|
/// -> auto | int | float | array
|
||||||
|
x: auto,
|
||||||
|
|
||||||
|
/// The position of the whiskers. The length of the whiskers is at most
|
||||||
|
/// `whisker-pos * (q3 - q1)` where `q1` and `q3` are the first and third quartils.
|
||||||
|
/// However, the whiskers always end at an actual data point, so the length can be
|
||||||
|
/// less then that. The default value of 1.5 is a very common convention established
|
||||||
|
/// by John Tukey in _Exploratory data analysis_ (1977).
|
||||||
|
/// -> int | float
|
||||||
|
whisker-pos: 1.5,
|
||||||
|
|
||||||
|
/// The width of the boxplots in $x$ data coordinates. This can be a constant width
|
||||||
|
/// applied to all boxplots or an array of widths matching the number of data sets.
|
||||||
|
/// -> int | float | array
|
||||||
|
width: 0.5,
|
||||||
|
|
||||||
|
/// How to fill the boxes.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// How to stroke the boxplot in general. Also see @boxplot.whisker, @boxplot.cap.
|
||||||
|
/// -> length | color | stroke | gradient | tiling | dictionary
|
||||||
|
stroke: 1pt + black,
|
||||||
|
|
||||||
|
/// How to stroke the line that indicates the median of the data.
|
||||||
|
/// -> length | color | stroke | gradient | tiling | dictionary
|
||||||
|
median: 1pt + orange,
|
||||||
|
|
||||||
|
/// Whether and how to display the mean value. The mean value can be
|
||||||
|
/// visualized with a mark (see @plot.mark) or a line like the median.
|
||||||
|
/// -> none | lq.mark | str | stroke
|
||||||
|
mean: none,
|
||||||
|
|
||||||
|
/// How to stroke the whiskers. If set to `auto`, the stroke is inherited from
|
||||||
|
/// @boxplot.stroke.
|
||||||
|
/// -> auto | length | color | stroke | gradient | tiling | dictionary
|
||||||
|
whisker: auto,
|
||||||
|
|
||||||
|
/// How to stroke the caps of the whiskers. If set to `auto`, the stroke is inherited
|
||||||
|
/// from @boxplot.stroke.
|
||||||
|
/// -> auto | length | color | stroke | gradient | tiling | dictionary
|
||||||
|
cap: auto,
|
||||||
|
|
||||||
|
/// The length of the whisker caps in $x$ data coordinates.
|
||||||
|
/// -> int | float
|
||||||
|
cap-length: 0.25,
|
||||||
|
|
||||||
|
/// Whether and how to display outliers. See @plot.mark.
|
||||||
|
/// -> none | lq.mark | str
|
||||||
|
outliers: "o",
|
||||||
|
|
||||||
|
/// The size of the marks used to visualize outliers.
|
||||||
|
/// -> length
|
||||||
|
outlier-size: 5pt,
|
||||||
|
|
||||||
|
/// How to fill outlier marks.
|
||||||
|
/// -> none | auto | color
|
||||||
|
outlier-fill: none,
|
||||||
|
|
||||||
|
/// How to stroke outlier marks.
|
||||||
|
/// -> stroke
|
||||||
|
outlier-stroke: black,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(data)
|
||||||
|
data = data.pos()
|
||||||
|
let num-boxplots = data.len()
|
||||||
|
|
||||||
|
if type(x) in (int, float, datetime) { x = (x,) }
|
||||||
|
else if x == auto { x = range(1, num-boxplots + 1) }
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(
|
||||||
|
x.len() == num-boxplots,
|
||||||
|
message: "The number of x coordinates does not match the number of data arrays"
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(width) in (int, float) { width = (width,) * num-boxplots }
|
||||||
|
assert(
|
||||||
|
width.len() == num-boxplots,
|
||||||
|
message: "The number of widths does not match the number of data arrays"
|
||||||
|
)
|
||||||
|
|
||||||
|
if whisker == auto { whisker = utility.if-none(stroke, std.stroke()) }
|
||||||
|
if cap == auto { cap = utility.if-none(stroke, std.stroke()) }
|
||||||
|
|
||||||
|
let statistics = data.map(boxplot-statistics.with(whiskers: whisker-pos))
|
||||||
|
|
||||||
|
|
||||||
|
let all-outliers = ()
|
||||||
|
if outliers != none {
|
||||||
|
all-outliers = statistics.map(boxplot => boxplot.outliers).flatten()
|
||||||
|
}
|
||||||
|
let ymax = calc.max(..statistics.map(x => x.whisker-high), ..all-outliers)
|
||||||
|
let ymin = calc.min(..statistics.map(x => x.whisker-low), ..all-outliers)
|
||||||
|
let xmin = x.at(0) - width.at(0)
|
||||||
|
let xmax = x.at(-1) + width.at(-1)
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
statistics: statistics,
|
||||||
|
label: label,
|
||||||
|
width: width,
|
||||||
|
style: (
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
cap: cap,
|
||||||
|
cap-length: cap-length,
|
||||||
|
whisker: whisker,
|
||||||
|
median: median,
|
||||||
|
mean: mean,
|
||||||
|
mark: if outliers != none { outliers },
|
||||||
|
mark-size: outlier-size,
|
||||||
|
outlier-fill: outlier-fill,
|
||||||
|
outlier-stroke: outlier-stroke,
|
||||||
|
),
|
||||||
|
plot: render-boxplot,
|
||||||
|
xlimits: () => (xmin, xmax),
|
||||||
|
ylimits: () => (ymin, ymax),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
259
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/colormesh.typ
vendored
Normal file
259
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/colormesh.typ
vendored
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../math.typ": sign, mesh
|
||||||
|
#import "../logic/sample-colors.typ": sample-colors
|
||||||
|
|
||||||
|
#let are-dimensions-all-equal(data) = {
|
||||||
|
if data.len() <= 1 { return true }
|
||||||
|
let x0 = data.first()
|
||||||
|
return data.slice(1).all(x => calc.abs(1 - x/x0) < 1e-8)
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert(are-dimensions-all-equal((1, 1, 1)))
|
||||||
|
#assert(not are-dimensions-all-equal((1, 1, 2)))
|
||||||
|
#assert(not are-dimensions-all-equal((1, 1, 1.0001)))
|
||||||
|
#assert(are-dimensions-all-equal((1, 1, 1.000000001)))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-colormesh(plot, transform) = {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return box(width: 100%, height: 100%, fill: plot.color.at(0))
|
||||||
|
}
|
||||||
|
|
||||||
|
let get-extents(a) = {
|
||||||
|
a.zip(a.slice(1)).map(((a, a1)) => a1 - a)
|
||||||
|
}
|
||||||
|
|
||||||
|
let get-size(i, a) = {
|
||||||
|
if i < a.len() - 1 {
|
||||||
|
let diff = a.at(i + 1) - a.at(i)
|
||||||
|
if i > 0 {
|
||||||
|
(a.at(i) - a.at(i - 1), diff)
|
||||||
|
} else {
|
||||||
|
(diff, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let diff = a.at(-1) - a.at(-2)
|
||||||
|
(diff, diff)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let widths = get-extents(plot.x)
|
||||||
|
let heights = get-extents(plot.y)
|
||||||
|
|
||||||
|
if are-dimensions-all-equal(widths) and are-dimensions-all-equal(heights) {
|
||||||
|
let (x0, xn) = (plot.x.at(0), plot.x.at(-1))
|
||||||
|
let (y0, yn) = (plot.y.at(0), plot.y.at(-1))
|
||||||
|
|
||||||
|
let w = widths.at(0)
|
||||||
|
let h = heights.at(0)
|
||||||
|
let (x1, y1) = transform(x0 - w / 2, y0 - h / 2)
|
||||||
|
let (x2, y2) = transform(xn + w / 2, yn + h / 2)
|
||||||
|
|
||||||
|
let img = image(
|
||||||
|
bytes(plot.color.map(c => rgb(c).components().map(x => int(x / 100% * 255))).join()),
|
||||||
|
format: (
|
||||||
|
encoding: "rgba8",
|
||||||
|
width: plot.x.len(),
|
||||||
|
height: plot.y.len(),
|
||||||
|
),
|
||||||
|
scaling: plot.interpolation,
|
||||||
|
width: calc.abs(x2 - x1),
|
||||||
|
fit: "stretch",
|
||||||
|
height: calc.abs(y2 - y1)
|
||||||
|
)
|
||||||
|
|
||||||
|
if x1 > x2 {
|
||||||
|
img = scale(origin: left, x: -100%, img)
|
||||||
|
}
|
||||||
|
if y1 > y2 {
|
||||||
|
img = scale(origin: top, y: -100%, img)
|
||||||
|
}
|
||||||
|
place(
|
||||||
|
top + left,
|
||||||
|
dx: x1,
|
||||||
|
dy: y1,
|
||||||
|
img
|
||||||
|
)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
plot.interpolation == "pixelated",
|
||||||
|
message: "For non-evenly-spaced color meshes, currently only the interpolation option \"pixelated\" is supported. "
|
||||||
|
)
|
||||||
|
|
||||||
|
for i in range(plot.x.len()) {
|
||||||
|
for j in range(plot.y.len()) {
|
||||||
|
let x = plot.x.at(i)
|
||||||
|
let y = plot.y.at(j)
|
||||||
|
|
||||||
|
let (w1, w2) = get-size(i, plot.x)
|
||||||
|
let (h1, h2) = get-size(j, plot.y)
|
||||||
|
let (x1, y1) = transform(x - w1 / 2, y + h2 / 2)
|
||||||
|
let (x2, y2) = transform(x + w2 / 2, y - h1 / 2)
|
||||||
|
let fill = plot.color.at(i + j * plot.x.len())
|
||||||
|
let width = x2 - x1
|
||||||
|
let height = y2 - y1
|
||||||
|
place(
|
||||||
|
dx: x1, dy: y1,
|
||||||
|
rect(width: width * 1.01, height: height * 1.01, fill: fill)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Plots a rectangular color mesh, e.g., a heatmap.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 4cm, height: 4cm,
|
||||||
|
/// lq.colormesh(
|
||||||
|
/// lq.linspace(-4, 4, num: 10),
|
||||||
|
/// lq.linspace(-4, 4, num: 10),
|
||||||
|
/// (x, y) => x * y,
|
||||||
|
/// map: color.map.magma
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// When the input `x` and `y` coordinate arrays are both evenly spaced, an
|
||||||
|
/// image is drawn instead of individual rectangles. This reduces the file size
|
||||||
|
/// and improves rendering in most cases. When either array is not evenly
|
||||||
|
/// spaced, the entire color mesh is drawn with individual rectangles.
|
||||||
|
///
|
||||||
|
#let colormesh(
|
||||||
|
|
||||||
|
/// A one-dimensional array of $x$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// A one-dimensional array of $y$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Specifies the $z$ coordinates (height) for all combinations of $x$ and $y$
|
||||||
|
/// coordinates. This can either be a
|
||||||
|
/// - two-dimensional $m×n$-array where $m$ is the length of @colormesh.y
|
||||||
|
/// and $n$ is the length of @colormesh.x (for each $y$ value, a row of $x$
|
||||||
|
/// values),
|
||||||
|
/// - or a function that takes an `x` and a `y` value and returns a
|
||||||
|
/// corresponding `z` coordinate.
|
||||||
|
/// Also see the function @mesh that can be used to create such meshes.
|
||||||
|
///
|
||||||
|
/// For masking, you can use `float.nan` values to hide individual cells of the color mesh.
|
||||||
|
/// -> array | function
|
||||||
|
z,
|
||||||
|
|
||||||
|
/// A color map in the form of a gradient or an array of colors to sample from.
|
||||||
|
/// -> array | gradient
|
||||||
|
map: color.map.viridis,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the first color of the color map.
|
||||||
|
/// If set to `auto`, it defaults to the minimum $z$ value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the last color of the color map.
|
||||||
|
/// If set to `auto`, it defaults to the maximum $z$ value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// Determines how values outside the range defined by @colormesh.min and
|
||||||
|
/// @colormesh.max are handled.
|
||||||
|
///
|
||||||
|
/// - `"clamp"`: Values below @colormesh.min are mapped to the first color of
|
||||||
|
/// the color map, values above @colormesh.max are mapped to the last color.
|
||||||
|
/// - `"mask"`: Values outside the range are not drawn and appear transparent.
|
||||||
|
///
|
||||||
|
/// -> "clamp" | "mask"
|
||||||
|
excess: "clamp",
|
||||||
|
|
||||||
|
/// The normalization method used to scale $z$ coordinates to the range
|
||||||
|
/// $[0,1]$ before mapping them to colors using the color map. This can be a
|
||||||
|
/// @scale, a string that is the identifier of a built-in scale or a function
|
||||||
|
/// that takes one argument (for example the argument `x => calc.log(x)`
|
||||||
|
/// would be equivalent to passing `"log"`). Note that the function does not
|
||||||
|
/// actually need to map the values to the interval $[0,1]$. Instead it
|
||||||
|
/// describes a scaling that is applied before the data set is _linearly_
|
||||||
|
/// scaled to the interval $[0,1]$.
|
||||||
|
/// -> lq.scale | str | function
|
||||||
|
norm: "linear",
|
||||||
|
|
||||||
|
/// Whether to apply smoothing or leave the color mesh pixelated. This is
|
||||||
|
/// currently only supported when @colormesh.x and @colormesh.y are evenly
|
||||||
|
/// spaced.
|
||||||
|
/// -> "pixelated" | "smooth"
|
||||||
|
interpolation: "pixelated",
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
) = {
|
||||||
|
|
||||||
|
if type(z) == function {
|
||||||
|
z = mesh(x, y, z)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.eq(
|
||||||
|
y.len(), z.len(),
|
||||||
|
message: "`colormesh`: The number of `y` coordinates and the number of rows in `z` must match. Found " + str(y.len()) + " != " + str(z.len())
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
type(z) == array and type(z.first()) == array,
|
||||||
|
message: "`colormesh`: `z` expects a 2D array"
|
||||||
|
)
|
||||||
|
assert.eq(
|
||||||
|
x.len(), z.first().len(),
|
||||||
|
message: "`colormesh`: The number of `x` coordinates and the row length in `z` must match. Found " + str(x.len()) + " != " + str(z.first().len())
|
||||||
|
)
|
||||||
|
|
||||||
|
assert(
|
||||||
|
excess in ("clamp", "mask"),
|
||||||
|
message: "`colormesh`: Invalid value for argument `excess`. Expected \"clamp\" or \"mask\", found \"" + str(excess) + "\""
|
||||||
|
)
|
||||||
|
|
||||||
|
let color = z.flatten()
|
||||||
|
|
||||||
|
let cinfo
|
||||||
|
if type(color.at(0, default: 0)) in (int, float) {
|
||||||
|
(color, cinfo) = sample-colors(
|
||||||
|
color,
|
||||||
|
map,
|
||||||
|
norm,
|
||||||
|
ignore-nan: true,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
excess: excess
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
cinfo: cinfo,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
z: z,
|
||||||
|
label: label,
|
||||||
|
color: color,
|
||||||
|
plot: render-colormesh,
|
||||||
|
interpolation: interpolation,
|
||||||
|
xlimits: () => (
|
||||||
|
1fr * (x.at(0) - 0.5 * (x.at(1) - x.at(0))),
|
||||||
|
1fr * (x.at(-1) + 0.5 * (x.at(-1) - x.at(-2)))
|
||||||
|
),
|
||||||
|
ylimits: () => (
|
||||||
|
1fr * (y.at(0) - 0.5 * (y.at(1) - y.at(0))),
|
||||||
|
1fr * (y.at(-1) + 0.5 * (y.at(-1) - y.at(-2)))
|
||||||
|
),
|
||||||
|
legend: true,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
222
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/contour.typ
vendored
Normal file
222
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/contour.typ
vendored
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../algorithm/contour.typ": close-path-at-boundaries, compute-polygon-orientation
|
||||||
|
#import "../logic/sample-colors.typ": sample-colors
|
||||||
|
#import "../math.typ": minmax, mesh
|
||||||
|
|
||||||
|
#let render-contour(plot, transform) = {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.line(length: 100%, stroke: plot.line-colors.first())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
if plot.fill {
|
||||||
|
|
||||||
|
let (xmin, xmax) = minmax(plot.x)
|
||||||
|
let (ymin, ymax) = minmax(plot.y)
|
||||||
|
let canvas-rect = ((xmax, ymax), (xmax, ymin), (xmin, ymin), (xmin, ymax), (xmax, ymax)) // right-turning curve
|
||||||
|
// assert.eq(compute-polygon-orientation(..canvas-rect), right)
|
||||||
|
|
||||||
|
for (i, contour) in plot.contours.enumerate() {
|
||||||
|
set curve(stroke: none, fill: plot.line-colors.at(i))
|
||||||
|
|
||||||
|
let to-closed-curve(contour) = {
|
||||||
|
if contour.len() == 0 { return () }
|
||||||
|
contour = contour.map(p => transform(..p))
|
||||||
|
return (
|
||||||
|
curve.move(contour.first()),
|
||||||
|
..contour.slice(1).map(curve.line),
|
||||||
|
curve.close(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if i == 0 {
|
||||||
|
contour = (canvas-rect,)
|
||||||
|
}
|
||||||
|
if contour.len() == 0 { continue }
|
||||||
|
if compute-polygon-orientation(..contour.first()) == left {
|
||||||
|
contour = (canvas-rect,) + contour
|
||||||
|
}
|
||||||
|
place(curve(
|
||||||
|
..contour.map(to-closed-curve).join()
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
|
||||||
|
for (i, contour) in plot.contours.enumerate() {
|
||||||
|
set curve(stroke: plot.line-colors.at(i))
|
||||||
|
set curve(stroke: plot.stroke)
|
||||||
|
for path in contour {
|
||||||
|
path = path.map(p => transform(..p))
|
||||||
|
place(std.curve(
|
||||||
|
curve.move(path.first()),
|
||||||
|
..path.slice(1).map(curve.line)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a contour plot for a 3-dimensional mesh. Given a set of `levels`,
|
||||||
|
/// a number of cuts through the mesh are computed automatically and displayed
|
||||||
|
/// as contour lines. Contour plots can be either just stroked
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 4cm, height: 4cm,
|
||||||
|
/// lq.contour(
|
||||||
|
/// lq.linspace(-5, 5, num: 12),
|
||||||
|
/// lq.linspace(-5, 5, num: 12),
|
||||||
|
/// (x, y) => x * y,
|
||||||
|
/// map: color.map.icefire,
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// or filled per-level.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 4cm, height: 4cm,
|
||||||
|
/// lq.contour(
|
||||||
|
/// lq.linspace(-5, 5, num: 12),
|
||||||
|
/// lq.linspace(-5, 5, num: 12),
|
||||||
|
/// (x, y) => x * y,
|
||||||
|
/// map: color.map.icefire,
|
||||||
|
/// fill: true
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let contour(
|
||||||
|
|
||||||
|
/// A one-dimensional array of $x$ data coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// A one-dimensional array of $y$ data coordinates.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Specifies the $z$ coordinates (height) for all combinations of $x$ and $y$
|
||||||
|
/// coordinates. This can either be a
|
||||||
|
/// - two-dimensional $m×n$-array where $m$ is the length of @contour.y
|
||||||
|
/// and $n$ is the length of @contour.x (for each $y$ value, a row of $x$
|
||||||
|
/// values),
|
||||||
|
/// - or a function that takes an `x` and a `y` value and returns a
|
||||||
|
/// corresponding `z` coordinate.
|
||||||
|
/// Also see the function @mesh that can be used to create such meshes.
|
||||||
|
/// -> array | function
|
||||||
|
z,
|
||||||
|
|
||||||
|
/// Specifies the levels to compute contours for. If this is an integer, an
|
||||||
|
/// according number of levels is automatically selected evenly from a ticking
|
||||||
|
/// pattern. TODO: unclear.
|
||||||
|
/// The desired levels can also be selected manually by passing an array of ($z$)
|
||||||
|
/// coordinates.
|
||||||
|
/// -> int | array
|
||||||
|
levels: 10,
|
||||||
|
|
||||||
|
/// Whether to fill the contour levels.
|
||||||
|
/// -> bool
|
||||||
|
fill: false,
|
||||||
|
|
||||||
|
/// How to stroke the contours in the cases that `fill: false`. If this
|
||||||
|
/// argument specifies a color, the coloring from the @contour.map is
|
||||||
|
/// overridden.
|
||||||
|
/// -> stroke
|
||||||
|
stroke: 0.7pt,
|
||||||
|
|
||||||
|
/// A color map in form of a gradient or an array of colors to sample from.
|
||||||
|
/// -> array | gradient
|
||||||
|
map: color.map.viridis,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the first color of the color map. If set
|
||||||
|
/// to `auto`, it defaults to the minimum $z$ value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the last color of the color map. If set
|
||||||
|
/// to `auto`, it defaults to the maximum $z$ value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// The normalization method used to scale $z$ coordinates to the range
|
||||||
|
/// $[0,1]$ before mapping them to colors using the color map. This can be a
|
||||||
|
/// @scale, a string that is the identifier of a built-in scale or a function
|
||||||
|
/// that takes one argument. See @colormesh.norm.
|
||||||
|
/// -> lq.scale | str | function
|
||||||
|
norm: "linear",
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(z) == function {
|
||||||
|
z = mesh(x, y, z)
|
||||||
|
}
|
||||||
|
assert.eq(
|
||||||
|
y.len(), z.len(),
|
||||||
|
message: "`colormesh`: The number of `y` coordinates and the number of rows in `z` must match. Found " + str(y.len()) + " != " + str(z.len())
|
||||||
|
)
|
||||||
|
assert(
|
||||||
|
type(z) == array and type(z.first()) == array,
|
||||||
|
message: "`colormesh`: `z` expects a 2D array"
|
||||||
|
)
|
||||||
|
assert.eq(
|
||||||
|
x.len(), z.first().len(),
|
||||||
|
message: "`colormesh`: The number of `x` coordinates and the row length in `z` must match. Found " + str(x.len()) + " != " + str(z.first().len())
|
||||||
|
)
|
||||||
|
|
||||||
|
let z-flat = z.flatten()
|
||||||
|
let (z0, z1) = (calc.min(..z-flat), calc.max(..z-flat))
|
||||||
|
if z0 == z1 { z0 -= 1; z1 += 1}
|
||||||
|
|
||||||
|
if type(levels) == int {
|
||||||
|
import "../logic/tick-locate.typ"
|
||||||
|
let range = tick-locate.linear(z0, z1, num-ticks-suggestion: levels)
|
||||||
|
levels = range.ticks
|
||||||
|
}
|
||||||
|
|
||||||
|
if min == auto { min = calc.min(..z-flat) }
|
||||||
|
if max == auto { max = calc.max(..z-flat) }
|
||||||
|
// if min == auto { min = calc.min(..levels) }
|
||||||
|
// if max == auto { max = calc.max(..levels) }
|
||||||
|
let (color, cinfo) = sample-colors(levels, map, norm, min: min, max: max)
|
||||||
|
|
||||||
|
|
||||||
|
import "@preview/komet:0.1.0"
|
||||||
|
let contours = komet.contour(x, y, z, levels)
|
||||||
|
|
||||||
|
if fill {
|
||||||
|
|
||||||
|
let boundaries = (xmin: x.first(), xmax: x.last(), ymin: y.first(), ymax: y.last())
|
||||||
|
|
||||||
|
contours = contours.map(paths => {
|
||||||
|
paths.map(close-path-at-boundaries.with(boundaries: boundaries)).filter(x => x != none)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
cinfo: cinfo,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
z: z,
|
||||||
|
levels: levels,
|
||||||
|
line-colors: color,
|
||||||
|
contours: contours,
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
label: label,
|
||||||
|
plot: render-contour,
|
||||||
|
xlimits: () => (x.at(0)*1fr, x.at(-1)*1fr),
|
||||||
|
ylimits: () => (y.at(0)*1fr, y.at(-1)*1fr),
|
||||||
|
legend: true,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
124
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/ellipse.typ
vendored
Normal file
124
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/ellipse.typ
vendored
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": compute-primitive-limits
|
||||||
|
#import "../logic/process-coordinates.typ": all-data-coordinates, convert-rect
|
||||||
|
#import "../process-styles.typ": twod-ify-alignment
|
||||||
|
|
||||||
|
|
||||||
|
/// Plots an ellipse or circle with origin `(x, y)`. The origin coordinates as well
|
||||||
|
/// as width and height can either be given as
|
||||||
|
/// - data coordinates (`int` or `float`),
|
||||||
|
/// - or absolute coordinates from the top left corner of the data area (`length`),
|
||||||
|
/// - or in percent relative to the data area (`ratio`),
|
||||||
|
/// - or a combination of the latter two (`relative`).
|
||||||
|
///
|
||||||
|
/// Note that coordinate types can also be mixed (e.g., a length for `x` and a scalar for `y`).
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// For example in order to access the center, you can write `(50%, 50%)`.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 3cm, height: 3cm,
|
||||||
|
/// lq.ellipse(2, 2, width: 10, height: 4, fill: yellow),
|
||||||
|
/// lq.ellipse(10, 4, width: 4, height: 4, fill: red),
|
||||||
|
/// lq.ellipse(50%, 50%, width: 45%, height: 45%, stroke: blue)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#let ellipse(
|
||||||
|
|
||||||
|
/// The x coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// The y coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// The width of the ellipse.
|
||||||
|
/// -> auto | float | relative
|
||||||
|
width: auto,
|
||||||
|
|
||||||
|
/// The height of the ellipse.
|
||||||
|
/// -> auto | float | relative
|
||||||
|
height: auto,
|
||||||
|
|
||||||
|
/// How to align the ellipse at the origin.
|
||||||
|
/// -> alignment
|
||||||
|
align: left + top,
|
||||||
|
|
||||||
|
/// How to fill the ellipse.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// How to stroke the ellipse.
|
||||||
|
/// -> auto | none | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// How much to pad the content of the ellipse. See the built-in [`std.ellipse#inset`](https://typst.app/docs/reference/visualize/ellipse/#parameters-inset).
|
||||||
|
/// -> relative | dictionary
|
||||||
|
inset: 5pt,
|
||||||
|
|
||||||
|
/// How much to expand the ellipse beyond its defined size.
|
||||||
|
/// See the built-in [`std.ellipse#outset`](https://typst.app/docs/reference/visualize/ellipse/#parameters-outset).
|
||||||
|
/// -> relative | dictionary
|
||||||
|
outset: 0pt,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
/// An optional body to place inside the ellipse. If `width` and/or `height` are set to `auto`, they will adapt to the content.
|
||||||
|
/// -> any
|
||||||
|
..body
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(body, fn: "ellipse")
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.ellipse(
|
||||||
|
width: 100%, height: 100%,
|
||||||
|
fill: fill, stroke: stroke
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x1, width, y1, height) = convert-rect(
|
||||||
|
x, y,
|
||||||
|
width, height,
|
||||||
|
transform,
|
||||||
|
align: twod-ify-alignment(align)
|
||||||
|
)
|
||||||
|
|
||||||
|
place(dx: x1, dy: y1,
|
||||||
|
std.ellipse(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
inset: inset,
|
||||||
|
outset: outset,
|
||||||
|
body.pos().at(0, default: none)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
xlimits: compute-primitive-limits.with((x, if all-data-coordinates((x, width)) { x + width } else { x })),
|
||||||
|
ylimits: compute-primitive-limits.with((y, if all-data-coordinates((y, height)) { y + height } else { y })),
|
||||||
|
label: label,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
138
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/fill-between.typ
vendored
Normal file
138
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/fill-between.typ
vendored
Normal file
@@ -0,0 +1,138 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../process-styles.typ": merge-strokes, merge-fills
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points, stepify
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../style/styling.typ": prepare-path
|
||||||
|
|
||||||
|
|
||||||
|
#let render-fill-between(plot, transform) = {
|
||||||
|
|
||||||
|
let y2 = if plot.y2 == none { (0,)*plot.x.len() } else { plot.y2 }
|
||||||
|
|
||||||
|
let (points, runs) = filter-nan-points(plot.x.zip(plot.y1, y2), generate-runs: true)
|
||||||
|
|
||||||
|
show: prepare-path.with(
|
||||||
|
fill: plot.style.fill,
|
||||||
|
stroke: plot.style.stroke,
|
||||||
|
element: polygon
|
||||||
|
)
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
polygon((0%, 0%), (0%, 100%), (100%, 100%), (100%, 0%))
|
||||||
|
} else {
|
||||||
|
for run in runs {
|
||||||
|
let there = run.map(x => x.slice(0,2))
|
||||||
|
let back = run.map(x => (x.at(0), x.at(2)))
|
||||||
|
if plot.style.step != none {
|
||||||
|
there = stepify(there, step: plot.style.step)
|
||||||
|
back = stepify(back, step: plot.style.step)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
place(polygon(
|
||||||
|
..((there + back.rev()).map(p => transform(..p))))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Fills the area between two graphs.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #let xs = lq.linspace(-1, 2)
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.fill-between(
|
||||||
|
/// xs,
|
||||||
|
/// xs.map(calc.sin),
|
||||||
|
/// y2: xs.map(calc.cos),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// or the area between one graph and the $x$-axis:
|
||||||
|
/// ```example
|
||||||
|
/// #let xs = lq.linspace(0, 3, num: 80)
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.fill-between(
|
||||||
|
/// label: [Maxwell-distribution],
|
||||||
|
/// xs,
|
||||||
|
/// xs.map(x => x*x*calc.exp(-x*x*1.3)),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let fill-between(
|
||||||
|
|
||||||
|
/// An array of $x$ data coordinates. Data coordinates need to be of type `int` or `float`.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Specifies either an array of $y$ coordinates or a function that takes an
|
||||||
|
/// `x` value and returns a corresponding `y` coordinate. The number of $x$
|
||||||
|
/// and $y$ coordinates must match.
|
||||||
|
/// -> array | function
|
||||||
|
y1,
|
||||||
|
|
||||||
|
/// An second array (or function) of $y$ data coordinates. If this is `none`,
|
||||||
|
/// the area between the coordinates `y1` and the $x$-axis is filled.
|
||||||
|
/// -> none | array | function
|
||||||
|
y2: none,
|
||||||
|
|
||||||
|
/// How to stroke the area.
|
||||||
|
/// -> none | stroke
|
||||||
|
stroke: none,
|
||||||
|
|
||||||
|
/// How to fill the area.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: auto,
|
||||||
|
|
||||||
|
/// Step mode for plotting the lines. See @plot.step.
|
||||||
|
/// -> none | start | end | center
|
||||||
|
step: none,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(y1) == function {
|
||||||
|
y1 = x.map(y1)
|
||||||
|
}
|
||||||
|
if type(y2) == function {
|
||||||
|
y2 = x.map(y2)
|
||||||
|
}
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(x, x, y1: y1, y2: y2, fn-name: "fill-between")
|
||||||
|
assert(step in (none, start, end, center))
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y1: y1,
|
||||||
|
y2: y2,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
step: step,
|
||||||
|
),
|
||||||
|
plot: render-fill-between,
|
||||||
|
xlimits: () => minmax(x),
|
||||||
|
ylimits: () => minmax(y1 + y2 + if y2 == none {(0,)}),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
202
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hbar.typ
vendored
Normal file
202
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hbar.typ
vendored
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
#import "bar.typ": *
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a horizontal bar plot from the given bar positions and lengths.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// yaxis: (subticks: none),
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (1, 2, 3, 2, 5, 3),
|
||||||
|
/// (1, 2, 3, 4, 5, 6),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Also see @bar.
|
||||||
|
#let hbar(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates specifying the bar lengths.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// An array of $y$ coordinates specifying the bar positions. The number of
|
||||||
|
/// $x$ and $y$ coordinates must match.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// How to fill the bars. This can be a single value applied to all bars or
|
||||||
|
/// an array with the same length as the coordinate arrays.
|
||||||
|
/// -> none | color | gradient | tiling | array
|
||||||
|
fill: auto,
|
||||||
|
|
||||||
|
/// How to stroke the bars. All values allowed by the built-in `rect`
|
||||||
|
/// function are also allowed here, see
|
||||||
|
/// [`std.rect#stroke`](https://typst.app/docs/reference/visualize/rect/#parameters-stroke).
|
||||||
|
/// In particular, note that by passing a dictionary, the individual sides
|
||||||
|
/// of the bars can be stroked independently.
|
||||||
|
/// -> auto | none | color | length | stroke | gradient | tiling | dictionary
|
||||||
|
stroke: none,
|
||||||
|
|
||||||
|
/// Alignment of the bars at the $y$ values.
|
||||||
|
/// #details[
|
||||||
|
/// Demonstration of the different alignment modes.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// yaxis: (subticks: none),
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (1,2,3,4,5), (1,2,3,4,5),
|
||||||
|
/// width: 0.2, fill: red,
|
||||||
|
/// align: top, label: "top"
|
||||||
|
/// ),
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (5,4,3,2,1), (1,2,3,4,5),
|
||||||
|
/// width: 0.2, fill: blue,
|
||||||
|
/// align: bottom, label: "bottom"
|
||||||
|
/// ),
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (2.5,) * 5, (1,2,3,4,5),
|
||||||
|
/// width: 0.2, fill: rgb("#AAEEAA99"),
|
||||||
|
/// align: center, label: "center"
|
||||||
|
/// ),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> top | center | bottom
|
||||||
|
align: center,
|
||||||
|
|
||||||
|
/// Width of the bars in data coordinates. The width can be set either to a
|
||||||
|
/// constant for all bars or per-bar by passing an array with
|
||||||
|
/// the same length as the coordinate arrays.
|
||||||
|
/// #details[
|
||||||
|
/// Example for a bar plot with varying bar widths.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (1, 2, 3, 2, 5),
|
||||||
|
/// (1, 2, 3, 4, 5),
|
||||||
|
/// width: (1, 0.5, 1, 0.5, 1),
|
||||||
|
/// fill: orange,
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> int | float | array
|
||||||
|
width: 0.8,
|
||||||
|
|
||||||
|
/// An offset to apply to all $y$ coordinates. This is equivalent to replacing
|
||||||
|
/// the array passed to @bar.y with `y.map(y => y + offset)`. Using an offset
|
||||||
|
/// can be useful to avoid overlaps when plotting multiple bar plots into one
|
||||||
|
/// diagram.
|
||||||
|
/// -> int | float | array
|
||||||
|
offset: 0,
|
||||||
|
|
||||||
|
|
||||||
|
/// Defines the $x$ coordinate of the baseline of the bars. This can either
|
||||||
|
/// be a constant value applied to all bars or it can be set per-bar by
|
||||||
|
/// passing an array with the same length as the coordinate arrays.
|
||||||
|
/// #details[
|
||||||
|
/// Bar plot with varying base.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// yaxis: (subticks: none),
|
||||||
|
/// lq.hbar(
|
||||||
|
/// (1, 2, 3, 0, 5),
|
||||||
|
/// (1, 2, 3, 4, 5),
|
||||||
|
/// base: (0, 1, 2, -1, 0),
|
||||||
|
/// fill: white,
|
||||||
|
/// stroke: 0.7pt
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> int | float | array
|
||||||
|
base: 0,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(
|
||||||
|
x, y, width: width, base: base, fn-name: "hbar"
|
||||||
|
)
|
||||||
|
|
||||||
|
if offset != 0 {
|
||||||
|
if type(offset) == array {
|
||||||
|
assertations.assert-matching-data-dimensions(
|
||||||
|
x, y, offset: offset, fn-name: "bar"
|
||||||
|
)
|
||||||
|
y = y.zip(offset).map(array.sum)
|
||||||
|
} else {
|
||||||
|
y = y.map(y => y + offset)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(base) != array {
|
||||||
|
base = (base,)
|
||||||
|
}
|
||||||
|
|
||||||
|
let offset-coeff = match(
|
||||||
|
align,
|
||||||
|
top, 0,
|
||||||
|
center, -0.5,
|
||||||
|
bottom, -1
|
||||||
|
)
|
||||||
|
|
||||||
|
let simple-lims() = vec.add(
|
||||||
|
minmax(y),
|
||||||
|
(offset-coeff*width, (1 + offset-coeff) * width)
|
||||||
|
)
|
||||||
|
|
||||||
|
let ylim = match-type(
|
||||||
|
width,
|
||||||
|
int: simple-lims,
|
||||||
|
float: simple-lims,
|
||||||
|
array: () => (
|
||||||
|
calc.min(..y.zip(width).map(((y, w)) => y + offset-coeff * w)),
|
||||||
|
calc.max(..y.zip(width).map(((y, w)) => y + (1 + offset-coeff) * w)),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
width: width,
|
||||||
|
offset-coeff: offset-coeff,
|
||||||
|
label: label,
|
||||||
|
base: base,
|
||||||
|
style: (
|
||||||
|
align: align,
|
||||||
|
stroke: stroke,
|
||||||
|
fill: fill
|
||||||
|
),
|
||||||
|
plot: render-bar.with(orientation: "horizontal"),
|
||||||
|
xlimits: () => bar-lim(x, base),
|
||||||
|
ylimits: () => ylim,
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
286
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hboxplot.typ
vendored
Normal file
286
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hboxplot.typ
vendored
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../utility.typ" as utility: match
|
||||||
|
#import "../algorithm/boxplot.typ": *
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
|
||||||
|
#let render-boxplot(plot, transform) = {
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.line(length: 100%, stroke: plot.style.stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let style = plot.style
|
||||||
|
|
||||||
|
|
||||||
|
for (i, statistics) in plot.statistics.enumerate() {
|
||||||
|
|
||||||
|
let y1 = plot.width.at(i) * -0.5
|
||||||
|
let y2 = plot.width.at(i) * 0.5
|
||||||
|
let y = plot.y.at(i)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let (q1, yy1) = transform(statistics.q1, y + y1)
|
||||||
|
let (q3, yy2) = transform(statistics.q3, y + y2)
|
||||||
|
let (median, middle) = transform(statistics.median, y)
|
||||||
|
let (whisker-low, _) = transform(statistics.whisker-low, y)
|
||||||
|
let (whisker-high, _) = transform(statistics.whisker-high, y)
|
||||||
|
let (_, xm1) = transform(statistics.q1, y - style.cap-length / 2)
|
||||||
|
let (_, xm2) = transform(statistics.q1, y + style.cap-length / 2)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let box-thickness = utility.if-auto(stroke(style.stroke).thickness, 1pt)
|
||||||
|
// y axis may be inverted:
|
||||||
|
if yy1 < yy2 { box-thickness *= -1 }
|
||||||
|
|
||||||
|
|
||||||
|
place(line(
|
||||||
|
start: (q1, middle),
|
||||||
|
end: (whisker-low, middle),
|
||||||
|
stroke: style.whisker
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (q3, middle),
|
||||||
|
end: (whisker-high, middle),
|
||||||
|
stroke: style.whisker
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (whisker-low, xm1),
|
||||||
|
end: (whisker-low, xm2),
|
||||||
|
stroke: style.cap
|
||||||
|
))
|
||||||
|
place(line(
|
||||||
|
start: (whisker-high, xm1),
|
||||||
|
end: (whisker-high, xm2),
|
||||||
|
stroke: style.cap
|
||||||
|
))
|
||||||
|
|
||||||
|
place(
|
||||||
|
dx: q1, dy: yy1,
|
||||||
|
rect(
|
||||||
|
height: yy2 - yy1,
|
||||||
|
width: q3 - q1,
|
||||||
|
fill: style.fill, stroke: style.stroke
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
place(line(
|
||||||
|
start: (median, yy1 - box-thickness / 2),
|
||||||
|
end: (median, yy2 + box-thickness / 2),
|
||||||
|
stroke: style.median
|
||||||
|
))
|
||||||
|
|
||||||
|
if style.mean != none {
|
||||||
|
let (mean, _) = transform(statistics.mean, y)
|
||||||
|
|
||||||
|
if type(style.mean) in (str, function) {
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mean,
|
||||||
|
size: plot.style.mark-size,
|
||||||
|
fill: plot.style.outlier-fill
|
||||||
|
)
|
||||||
|
set mark(stroke: plot.style.outlier-stroke)
|
||||||
|
place(dx: mean, dy: middle, mark())
|
||||||
|
|
||||||
|
} else {
|
||||||
|
place(line(
|
||||||
|
start: (mean, yy1 - box-thickness/2),
|
||||||
|
end: (mean, yy2 + box-thickness/2),
|
||||||
|
stroke: style.mean
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if plot.style.mark != none {
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mark,
|
||||||
|
size: plot.style.mark-size,
|
||||||
|
fill: plot.style.outlier-fill
|
||||||
|
)
|
||||||
|
set mark(stroke: plot.style.outlier-stroke)
|
||||||
|
for outlier in statistics.outliers {
|
||||||
|
let (x, _) = transform(outlier, y + y1)
|
||||||
|
place(dx: x, dy: middle, mark())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes and visualizes one or more boxplots from datasets.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.hboxplot(
|
||||||
|
/// stroke: blue.darken(50%),
|
||||||
|
/// (1, 2, 3, 4, 5, 6, 7, 8, 9, 21, 19),
|
||||||
|
/// range(1, 30),
|
||||||
|
/// (1, 28, 25, 30),
|
||||||
|
/// (1, 2, 3, 4, 5, 6, 32),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// This is the horizontal version of @boxplot. There you can find a more
|
||||||
|
/// detailed documentation.
|
||||||
|
#let hboxplot(
|
||||||
|
|
||||||
|
/// One or more data sets to generate a boxplot from. A data set can either be
|
||||||
|
/// - an array of values (in this case all statistics are computed automatically) or
|
||||||
|
/// - a dictionary with the mandatory keys `median`, `q1`, `q3`,
|
||||||
|
/// `whisker-low`, and `whisker-high` and optional keys `mean` and
|
||||||
|
/// `outliers`. This method is useful for precomputed distributions,
|
||||||
|
/// especially data sets that are too large for processing in Typst.
|
||||||
|
///
|
||||||
|
/// -> array | dictionary
|
||||||
|
..data,
|
||||||
|
|
||||||
|
/// The $y$ coordinate(s) to draw the boxplots at. If set to `auto`, boxplots will
|
||||||
|
/// be created at integer positions starting with 1.
|
||||||
|
/// -> auto | int | float | array
|
||||||
|
y: auto,
|
||||||
|
|
||||||
|
/// The position of the whiskers. The length of the whiskers is at most
|
||||||
|
/// `whisker-pos * (q3 - q1)` where `q1` and `q3` are the first and third quartils.
|
||||||
|
/// However, the whiskers always end at an actual data point, so the length can be
|
||||||
|
/// less then that. The default value of 1.5 is a very common convention established
|
||||||
|
/// by John Tukey in _Exploratory data analysis_ (1977).
|
||||||
|
/// -> int | float
|
||||||
|
whisker-pos: 1.5,
|
||||||
|
|
||||||
|
/// The width of the boxplots in $y$ data coordinates. This can be a constant width
|
||||||
|
/// applied to all boxplots or an array of widths matching the number of data sets.
|
||||||
|
/// -> int | float | array
|
||||||
|
width: 0.5,
|
||||||
|
|
||||||
|
/// How to fill the boxes.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// How to stroke the boxplot in general. Also see @boxplot.whisker, @boxplot.cap.
|
||||||
|
/// -> length | color | stroke | gradient | tiling | dictionary
|
||||||
|
stroke: 1pt + black,
|
||||||
|
|
||||||
|
/// How to stroke the line that indicates the median of the data.
|
||||||
|
/// -> length | color | stroke | gradient | tiling | dictionary
|
||||||
|
median: 1pt + orange,
|
||||||
|
|
||||||
|
/// Whether and how to display the mean value. The mean value can be
|
||||||
|
/// visualized with a mark (see @plot.mark) or a line like the median.
|
||||||
|
/// -> none | lq.mark | str | stroke
|
||||||
|
mean: none,
|
||||||
|
|
||||||
|
/// How to stroke the whiskers. If set to `auto`, the stroke is inherited from
|
||||||
|
/// @boxplot.stroke.
|
||||||
|
/// -> auto | length | color | stroke | gradient | tiling | dictionary
|
||||||
|
whisker: auto,
|
||||||
|
|
||||||
|
/// How to stroke the caps of the whiskers. If set to `auto`, the stroke is inherited
|
||||||
|
/// from @boxplot.stroke.
|
||||||
|
/// -> auto | length | color | stroke | gradient | tiling | dictionary
|
||||||
|
cap: auto,
|
||||||
|
|
||||||
|
/// The length of the whisker caps in $y$ data coordinates.
|
||||||
|
/// -> int | float
|
||||||
|
cap-length: 0.25,
|
||||||
|
|
||||||
|
/// Whether and how to display outliers. See @plot.mark.
|
||||||
|
/// -> none | lq.mark | str
|
||||||
|
outliers: "o",
|
||||||
|
|
||||||
|
/// The size of the marks used to visualize outliers.
|
||||||
|
/// -> length
|
||||||
|
outlier-size: 5pt,
|
||||||
|
|
||||||
|
/// How to fill outlier marks.
|
||||||
|
/// -> none | auto | color
|
||||||
|
outlier-fill: none,
|
||||||
|
|
||||||
|
/// How to stroke outlier marks.
|
||||||
|
/// -> stroke
|
||||||
|
outlier-stroke: black,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(data)
|
||||||
|
data = data.pos()
|
||||||
|
let num-boxplots = data.len()
|
||||||
|
|
||||||
|
if type(y) in (int, float) { y = (y,) }
|
||||||
|
else if y == auto { y = range(1, num-boxplots + 1) }
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
assert(
|
||||||
|
y.len() == num-boxplots,
|
||||||
|
message: "The number of y coordinates does not match the number of data arrays"
|
||||||
|
)
|
||||||
|
|
||||||
|
if type(width) in (int, float) { width = (width,) * num-boxplots }
|
||||||
|
assert(
|
||||||
|
width.len() == num-boxplots,
|
||||||
|
message: "The number of widths does not match the number of data arrays"
|
||||||
|
)
|
||||||
|
|
||||||
|
if whisker == auto { whisker = utility.if-none(stroke, std.stroke()) }
|
||||||
|
if cap == auto { cap = utility.if-none(stroke, std.stroke()) }
|
||||||
|
|
||||||
|
let statistics = data.map(boxplot-statistics.with(whiskers: whisker-pos))
|
||||||
|
|
||||||
|
|
||||||
|
let all-outliers = ()
|
||||||
|
if outliers != none {
|
||||||
|
all-outliers = statistics.map(boxplot => boxplot.outliers).flatten()
|
||||||
|
}
|
||||||
|
let xmax = calc.max(..statistics.map(x => x.whisker-high), ..all-outliers)
|
||||||
|
let xmin = calc.min(..statistics.map(x => x.whisker-low), ..all-outliers)
|
||||||
|
let ymin = y.at(0) - width.at(0)
|
||||||
|
let ymax = y.at(-1) + width.at(-1)
|
||||||
|
|
||||||
|
(
|
||||||
|
y: y,
|
||||||
|
statistics: statistics,
|
||||||
|
label: label,
|
||||||
|
width: width,
|
||||||
|
style: (
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
cap: cap,
|
||||||
|
cap-length: cap-length,
|
||||||
|
whisker: whisker,
|
||||||
|
median: median,
|
||||||
|
mean: mean,
|
||||||
|
mark: if outliers != none { outliers },
|
||||||
|
mark-size: outlier-size,
|
||||||
|
outlier-fill: outlier-fill,
|
||||||
|
outlier-stroke: outlier-stroke,
|
||||||
|
),
|
||||||
|
plot: render-boxplot,
|
||||||
|
xlimits: () => (xmin, xmax),
|
||||||
|
ylimits: () => (ymin, ymax),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
113
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hlines.typ
vendored
Normal file
113
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hlines.typ
vendored
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
#import "../process-styles.typ": merge-strokes
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-hlines(plot, transform) = {
|
||||||
|
let min = plot.min
|
||||||
|
let max = plot.max
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return line(length: 100%, stroke: plot.style.stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
if min == auto { min = 0% }
|
||||||
|
else {
|
||||||
|
(min, _) = transform(min, 1)
|
||||||
|
}
|
||||||
|
if max == auto { max = 100% }
|
||||||
|
else {
|
||||||
|
(max, _) = transform(max, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
for y in plot.y {
|
||||||
|
let (_, yy) = transform(1, y)
|
||||||
|
place(line(start: (min, yy), end: (max, yy), stroke: plot.style.stroke))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Draws a set of horizontal lines into the diagram.
|
||||||
|
///
|
||||||
|
/// By default, the lines are indefinite and span the entire width of the
|
||||||
|
/// diagram, independent of the limits, however, through `min` and `max`,
|
||||||
|
/// beginning and end of the line can be fixed to an $x$ coordinate,
|
||||||
|
/// respectively.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// xlim: (0, 7),
|
||||||
|
/// lq.hlines(1, 1.1, stroke: teal, label: "Indefinite"),
|
||||||
|
/// lq.hlines(2, stroke: blue, min: 2, label: "Fixed start"),
|
||||||
|
/// lq.hlines(3, stroke: purple, max: 2, label: "Fixed end"),
|
||||||
|
/// lq.hlines(4, stroke: red, min: 1, max: 3, label: "Fixed"),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let hlines(
|
||||||
|
|
||||||
|
/// The $y$ coordinate(s) of one or more horizontal lines to draw.
|
||||||
|
/// -> int | float
|
||||||
|
..y,
|
||||||
|
|
||||||
|
/// The beginning of the line as an $x$ coordinate. If set to `auto`, the line will
|
||||||
|
/// always start at the left edge of the diagram.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// The end of the line as an $x$ coordinate. If set to `auto`, the line will
|
||||||
|
/// always end at the right edge of the diagram.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// How to stroke the lines.
|
||||||
|
/// -> stroke
|
||||||
|
stroke: black,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(y)
|
||||||
|
y = y.pos()
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
stroke = merge-strokes(stroke)
|
||||||
|
|
||||||
|
let xlimits = none
|
||||||
|
if min != auto {
|
||||||
|
xlimits = (min, min)
|
||||||
|
}
|
||||||
|
if max != auto {
|
||||||
|
xlimits = (if min == auto { max } else { min }, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
y: y,
|
||||||
|
label: label,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
style: (
|
||||||
|
stroke: stroke,
|
||||||
|
),
|
||||||
|
plot: render-hlines,
|
||||||
|
xlimits: () => xlimits,
|
||||||
|
ylimits: () => minmax(y),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
157
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hstem.typ
vendored
Normal file
157
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/hstem.typ
vendored
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": bar-lim
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../process-styles.typ": merge-strokes, merge-fills
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../utility.typ": if-auto
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark, prepare-line
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-hstem(plot, transform) = {
|
||||||
|
let marker = mark()
|
||||||
|
set line(stroke: plot.style.stroke)
|
||||||
|
|
||||||
|
let (ymin, ymax) = (plot.ylimits)()
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
ymin = 0
|
||||||
|
ymax = 1
|
||||||
|
plot.x = (.75,)
|
||||||
|
plot.y = (.5,)
|
||||||
|
plot.base = .25
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x0, y1) = transform(plot.base, ymin)
|
||||||
|
let (x0, y2) = transform(plot.base, ymax)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mark,
|
||||||
|
color: merge-fills(plot.style.color),
|
||||||
|
size: plot.style.mark-size
|
||||||
|
)
|
||||||
|
show: prepare-line.with(
|
||||||
|
stroke: merge-strokes(plot.style.stroke, plot.style.color)
|
||||||
|
)
|
||||||
|
|
||||||
|
let points = filter-nan-points(plot.x.zip(plot.y)).map(p => transform(..p))
|
||||||
|
|
||||||
|
points.map(((x, y)) => place(line(start: (x0, y), end: (x, y)))).join()
|
||||||
|
|
||||||
|
if plot.style.base-stroke != none {
|
||||||
|
set line(stroke: (cap: "square"))
|
||||||
|
place(line(
|
||||||
|
start: (x0, y1),
|
||||||
|
end: (x0, y2),
|
||||||
|
stroke: plot.style.base-stroke
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
points.map(((x, y)) => place(dx: x, dy: y, marker)).join()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a horizontal stem plot.
|
||||||
|
/// ```example
|
||||||
|
/// #let ys = lq.linspace(0, 10, num: 20)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.hstem(
|
||||||
|
/// ys.map(calc.cos),
|
||||||
|
/// ys,
|
||||||
|
/// color: orange,
|
||||||
|
/// mark: "d",
|
||||||
|
/// base-stroke: black,
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Also see @stem for vertical stem plots.
|
||||||
|
#let hstem(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// An array of $y$ coordinates. The number of $x$ and $y$ coordinates
|
||||||
|
/// must match.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Combined color for line and marks. See also the parameter @hstem.line which takes precedence over `color`, if set.
|
||||||
|
/// -> auto | color
|
||||||
|
color: auto,
|
||||||
|
|
||||||
|
/// The line style to use for this plot (takes precedence over @hstem.color).
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// The mark to use to mark data points. See @plot.mark.
|
||||||
|
/// -> auto | none | lq.mark | str
|
||||||
|
mark: auto,
|
||||||
|
|
||||||
|
/// Size of the marks.
|
||||||
|
/// -> length
|
||||||
|
mark-size: auto,
|
||||||
|
|
||||||
|
/// Defines the $x$ coordinate of the base line.
|
||||||
|
/// -> int | float
|
||||||
|
base: 0,
|
||||||
|
|
||||||
|
/// How to stroke the base line.
|
||||||
|
/// -> stroke
|
||||||
|
base-stroke: red,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(x, y, fn-name: "hstem")
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
base: base,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
color: color,
|
||||||
|
mark: mark,
|
||||||
|
mark-size: mark-size,
|
||||||
|
stroke: merge-strokes(stroke, color),
|
||||||
|
base-stroke: base-stroke
|
||||||
|
),
|
||||||
|
plot: render-hstem,
|
||||||
|
xlimits: () => bar-lim(x, (base,)),
|
||||||
|
ylimits: () => minmax(y),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
119
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/line.typ
vendored
Normal file
119
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/line.typ
vendored
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
#import "../logic/process-coordinates.typ": convert-vertex
|
||||||
|
#import "../logic/limits.typ": compute-primitive-limits
|
||||||
|
#import "@preview/tiptoe:0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
/// Draws a line into the data area.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 3cm, height: 3cm,
|
||||||
|
/// lq.line((1, 2), (5, 4))
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// The coordinates can also be given relative to the data area
|
||||||
|
/// (`0%` means at the very left/top and `100%` at the very right/bottom)
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 3cm, height: 3cm,
|
||||||
|
/// lq.line((0%, 0%), (100%, 100%))
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// or mixed between relative, absolute and data values.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 3cm, height: 3cm,
|
||||||
|
/// lq.line(
|
||||||
|
/// stroke: (paint: blue, dash: "dashed"),
|
||||||
|
/// (1, 100%), (4, 10pt)
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Arrow tips and tails can be added to a line through the arguments `tip` and `toe`.
|
||||||
|
/// This is supported via the Typst package
|
||||||
|
/// #link("https://typst.app/universe/package/tiptoe")[tiptoe].
|
||||||
|
/// ```example
|
||||||
|
/// #import "@preview/tiptoe:0.3.1"
|
||||||
|
/// #let xs = lq.arange(-5, 6)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(xs, xs.map(x => x*x)),
|
||||||
|
/// lq.line(
|
||||||
|
/// tip: tiptoe.stealth,
|
||||||
|
/// toe: tiptoe.bar,
|
||||||
|
/// (0, 10), (4, 16)
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#let line(
|
||||||
|
|
||||||
|
/// The start point of the line. Coordinates can be given as
|
||||||
|
/// - data coordinates (`int` or `float`),
|
||||||
|
/// - or absolute coordinates from the top left corner of the data area
|
||||||
|
/// (`length`),
|
||||||
|
/// - or in percent relative to the data area (`ratio`),
|
||||||
|
/// - or a combination of the latter two (`relative`).
|
||||||
|
/// -> array
|
||||||
|
start,
|
||||||
|
|
||||||
|
/// The end point of the line, see @line.start.
|
||||||
|
/// -> array
|
||||||
|
end,
|
||||||
|
|
||||||
|
/// How to stroke the line.
|
||||||
|
/// -> stroke
|
||||||
|
stroke: stroke(),
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Places an arrow tip on the line. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
tip: none,
|
||||||
|
|
||||||
|
/// Places an arrow tail on the line. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
toe: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram objects.
|
||||||
|
/// See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let vertices = (start, end)
|
||||||
|
(
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.line(length: 100%, stroke: stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (start, end) = vertices.map(convert-vertex.with(transform: transform))
|
||||||
|
if tip == none and toe == none {
|
||||||
|
return place(std.line(
|
||||||
|
stroke: stroke,
|
||||||
|
start: start, end: end
|
||||||
|
))
|
||||||
|
}
|
||||||
|
place(tiptoe.line(
|
||||||
|
stroke: stroke,
|
||||||
|
start: start, end: end,
|
||||||
|
tip: tip, toe: toe
|
||||||
|
))
|
||||||
|
},
|
||||||
|
xlimits: compute-primitive-limits.with(vertices.map(x => x.at(0))),
|
||||||
|
ylimits: compute-primitive-limits.with(vertices.map(x => x.at(1))),
|
||||||
|
label: label,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
204
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/path.typ
vendored
Normal file
204
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/path.typ
vendored
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": compute-primitive-limits
|
||||||
|
#import "../logic/process-coordinates.typ": convert-bezier-curve, transform-point
|
||||||
|
#import "../math.typ": vec
|
||||||
|
#import "@preview/tiptoe:0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let path-to-curve(
|
||||||
|
..vertices,
|
||||||
|
closed: false
|
||||||
|
) = {
|
||||||
|
vertices = vertices.pos()
|
||||||
|
if vertices.len() == 0 { return }
|
||||||
|
|
||||||
|
let is-vertex(v) = type(v) == array and type(v.first()) != array
|
||||||
|
let extract-vertex(v) = {
|
||||||
|
if is-vertex(v) { v }
|
||||||
|
else { v.first() }
|
||||||
|
}
|
||||||
|
|
||||||
|
let curve-elements = ()
|
||||||
|
let start-in = none
|
||||||
|
let out = none
|
||||||
|
for vertex in vertices {
|
||||||
|
let v = extract-vertex(vertex)
|
||||||
|
|
||||||
|
if is-vertex(vertex) {
|
||||||
|
if out == none {
|
||||||
|
curve-elements.push((v,))
|
||||||
|
} else {
|
||||||
|
curve-elements.push((out, none, v))
|
||||||
|
out = none
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if vertex.len() == 2 {
|
||||||
|
vertex.push(vec.multiply(vertex.at(1), -1))
|
||||||
|
// now its definitely a "cubic"!
|
||||||
|
}
|
||||||
|
if curve-elements.len() == 0 {
|
||||||
|
curve-elements.push((v,))
|
||||||
|
start-in = vec.add(v, vertex.at(1))
|
||||||
|
} else if out == none {
|
||||||
|
curve-elements.push((auto, vec.add(v, vertex.at(1)), v))
|
||||||
|
out = auto
|
||||||
|
} else {
|
||||||
|
curve-elements.push((out, vec.add(v, vertex.at(1)), v))
|
||||||
|
}
|
||||||
|
out = vec.add(v, vertex.at(2))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let to-curve-element(x) = {
|
||||||
|
if x.len() == 1 { curve.line(..x) }
|
||||||
|
else if x.len() == 2 { curve.quad(..x) }
|
||||||
|
else if x.len() == 3 { curve.cubic(..x) }
|
||||||
|
}
|
||||||
|
curve-elements = curve-elements.map(to-curve-element)
|
||||||
|
let start = extract-vertex(vertices.first())
|
||||||
|
if closed {
|
||||||
|
if out != none or start-in != none {
|
||||||
|
curve-elements.push(curve.cubic(out, start-in, start))
|
||||||
|
}
|
||||||
|
curve-elements.push(curve.close(mode: "straight"))
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
curve.move(start),
|
||||||
|
..curve-elements,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Draws a path into the data area. Each vertex may be given as data coordinates,
|
||||||
|
/// as percentage relative to the data area or in absolute lengths (see @rect).
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// height: 3.8cm,
|
||||||
|
/// width: 4cm,
|
||||||
|
/// lq.path(
|
||||||
|
/// ((0, 1), (0, -1)),
|
||||||
|
/// ((.5, 1), (0, 1)),
|
||||||
|
/// ((0,-1), (0, 1), (0, 1)),
|
||||||
|
/// ((-.5, 1), (0, -1)),
|
||||||
|
/// ((0, 1), (0, 1)),
|
||||||
|
/// stroke: red + 2pt
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#let path(
|
||||||
|
|
||||||
|
/// Vertices and curve elements. See the Typst built-in `path` function.
|
||||||
|
/// -> array
|
||||||
|
..vertices,
|
||||||
|
|
||||||
|
/// How to fill the path.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// How to stroke the path.
|
||||||
|
/// -> auto | none | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// Whether to close the path.
|
||||||
|
/// -> bool
|
||||||
|
closed: false,
|
||||||
|
|
||||||
|
/// Places an arrow tip on the curve. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
tip: none,
|
||||||
|
|
||||||
|
/// Places an arrow tail on the curve. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
toe: none,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram objects.
|
||||||
|
/// See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(vertices, fn: "path")
|
||||||
|
|
||||||
|
let sub(p, q) = (p.at(0) - q.at(0), p.at(1) - q.at(1))
|
||||||
|
let add(p, q) = (p.at(0) + q.at(0), p.at(1) + q.at(1))
|
||||||
|
|
||||||
|
vertices = vertices.pos()
|
||||||
|
let all-points = vertices.enumerate().map(((i, v)) => {
|
||||||
|
if type(v.at(0)) != array { return (v,) }
|
||||||
|
if v.len() == 3 {
|
||||||
|
return (v.at(0),) + v.slice(1).map(c => {
|
||||||
|
if not c.map(type).all(x => x in (int, float)) { return v.at(0) }
|
||||||
|
add(v.at(0), c)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let vs = (v.at(0),)
|
||||||
|
if i != 0 and v.at(1).map(type).all(x => x in (int, float)){
|
||||||
|
vs.push(add(v.at(0), v.at(1)))
|
||||||
|
}
|
||||||
|
if i != vertices.len() - 1 {
|
||||||
|
// vs.push(sub(v.at(0), v.at(1)))
|
||||||
|
}
|
||||||
|
return vs
|
||||||
|
}).join()
|
||||||
|
// vertices = all-points
|
||||||
|
// let all-points = vertices
|
||||||
|
(
|
||||||
|
vertices: vertices,
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.rect(
|
||||||
|
width: 100%, height: 100%,
|
||||||
|
fill: fill, stroke: stroke
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let new-vertices = vertices.map(v => {
|
||||||
|
if type(v.at(0)) != array { return transform-point(..v, transform) }
|
||||||
|
return convert-bezier-curve(v, transform)
|
||||||
|
let p = transform-point(..v.at(0), transform)
|
||||||
|
let cs = v.slice(1).map(c => {
|
||||||
|
let cabs = add(v.at(0), c)
|
||||||
|
let cabs = c
|
||||||
|
sub(transform-point(..cabs, transform), p)
|
||||||
|
})
|
||||||
|
(p,) + cs
|
||||||
|
})
|
||||||
|
let segments = path-to-curve(
|
||||||
|
closed: closed,
|
||||||
|
..new-vertices
|
||||||
|
)
|
||||||
|
let curve = std.curve
|
||||||
|
if tip != none or toe != none {
|
||||||
|
curve = tiptoe.curve.with(tip: tip, toe: toe)
|
||||||
|
}
|
||||||
|
|
||||||
|
place(
|
||||||
|
curve(
|
||||||
|
..segments,
|
||||||
|
fill: fill, stroke: stroke,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
xlimits: compute-primitive-limits.with(all-points.map(x => x.at(0))),
|
||||||
|
ylimits: compute-primitive-limits.with(all-points.map(x => x.at(1))),
|
||||||
|
label: label,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
114
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/place.typ
vendored
Normal file
114
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/place.typ
vendored
Normal file
@@ -0,0 +1,114 @@
|
|||||||
|
#import "../process-styles.typ": twod-ify-alignment
|
||||||
|
#import "../logic/process-coordinates.typ": transform-point
|
||||||
|
|
||||||
|
|
||||||
|
/// Places any type of content in the data area. The coordinates of the origin
|
||||||
|
/// can be given as data coordinates, as percentage relative to the data area
|
||||||
|
/// or in absolute lengths (see @rect).
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.place(0.5, 0.5)[Hello],
|
||||||
|
/// lq.place(0%, 0.3, align: left)[
|
||||||
|
/// Text placed at `x: 0%` and `y: 0.3`
|
||||||
|
/// ]
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// When annotating data points, it can be particularly useful to use the built-in
|
||||||
|
/// #link("https://typst.app/docs/reference/layout/pad/")[`pad`] function to
|
||||||
|
/// add some space.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot((1, 2, 3, 4), (3, 5, 1, 3)),
|
||||||
|
/// lq.place(2, 5, align: left, pad(.7em)[max]),
|
||||||
|
/// lq.place(3, 1, align: right, pad(.7em)[min])
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// Unlike other plotting commands, @place is not clipped to the data area by
|
||||||
|
/// default and the z-index is higher, making the placed content appear
|
||||||
|
/// on top of most other diagram objects.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot((1, 2, 3), (1, 2.1, 3)),
|
||||||
|
/// lq.place(
|
||||||
|
/// 10%, 0%,
|
||||||
|
/// align: left,
|
||||||
|
/// box(fill: yellow, inset: 2pt)[
|
||||||
|
/// Here comes the plot🎶 ↓
|
||||||
|
/// ]
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// Clipping can be activated via @place.clip and the z-index can be changed
|
||||||
|
/// through @place.z-index.
|
||||||
|
///
|
||||||
|
///
|
||||||
|
/// With @place, a smaller plot can also be placed within another plot.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #let xs = lq.linspace(-5, 5, num: 20)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot((1,2,3), (1,2,3)),
|
||||||
|
/// lq.place(25%, 50%,
|
||||||
|
/// lq.diagram(
|
||||||
|
/// title: [mini],
|
||||||
|
/// fill: white,
|
||||||
|
/// width: 40pt, height: 30pt,
|
||||||
|
/// lq.plot(xs, xs.map(x => x*x))
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#let place(
|
||||||
|
|
||||||
|
/// The $x$ coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// The $y$ coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// The content to place in the data area.
|
||||||
|
/// -> any
|
||||||
|
body,
|
||||||
|
|
||||||
|
/// How to align the content at the origin coordinates.
|
||||||
|
/// -> alignment
|
||||||
|
align: center + horizon,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: false,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of the content in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 21,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
align: align,
|
||||||
|
body: body,
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
let (px, py) = transform-point(x, y, transform)
|
||||||
|
std.place(
|
||||||
|
dx: px, dy: py,
|
||||||
|
std.place(twod-ify-alignment(align), body),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
id: "place",
|
||||||
|
xlimits: () => none,
|
||||||
|
ylimits: () => none,
|
||||||
|
label: none,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
495
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/plot.typ
vendored
Normal file
495
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/plot.typ
vendored
Normal file
@@ -0,0 +1,495 @@
|
|||||||
|
#import "../algorithm/bezier-interpolation.typ": bezier-splines
|
||||||
|
#import "../logic/limits.typ": plot-lim
|
||||||
|
#import "../process-styles.typ": merge-strokes, merge-fills
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points, stepify
|
||||||
|
#import "../utility.typ": if-auto
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark, prepare-path
|
||||||
|
#import "../model/errorbar.typ": errorbar
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
#let get-errorbar-stroke(base-stroke) = {
|
||||||
|
if base-stroke == auto { return stroke() }
|
||||||
|
if base-stroke == none { return stroke() }
|
||||||
|
return stroke()
|
||||||
|
return stroke(thickness: base-stroke.thickness, paint: base-stroke.paint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process error inputs of the form as documented in @plot.xerr.
|
||||||
|
#let process-errors(err, n /* basically x.len() */, kind: "x") = {
|
||||||
|
|
||||||
|
if type(err) in (int, float) {
|
||||||
|
err = (p: (err,) * n, m: (err,) * n)
|
||||||
|
|
||||||
|
} else if type(err) == dictionary {
|
||||||
|
assert(
|
||||||
|
"m" in err and "p" in err,
|
||||||
|
message: "Error bar dictionaries must contain both \"p\" and \"m\""
|
||||||
|
)
|
||||||
|
if err.keys().len() != 2 {
|
||||||
|
let key = err.keys().filter(x => x not in ("m", "p")).first()
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "Errorbar dictionary contains unexpected key \"" + key + "\", expected \"p\" and \"m\""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(err.m) in (int, float) {
|
||||||
|
err.m = (err.m,) * n
|
||||||
|
} else if type(err.m) == array {
|
||||||
|
assert(
|
||||||
|
err.m.len() == n,
|
||||||
|
message: "The length of `" + kind + "err.m` does not match the number of data points"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "`" + kind + "err.m` expects a float or an array")
|
||||||
|
}
|
||||||
|
if type(err.p) in (int, float) {
|
||||||
|
err.p = (err.p,) * n
|
||||||
|
} else if type(err.p) == array {
|
||||||
|
assert(
|
||||||
|
err.p.len() == n,
|
||||||
|
message: "The length of `" + kind + "err.p` does not match the number of data points"
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
assert(false, message: "`" + kind + "err.p` expects a float or an array")
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if type(err) == array {
|
||||||
|
assert(
|
||||||
|
err.len() == n,
|
||||||
|
message: "The length of `" + kind + "err` (" + str(err.len()) + ") does not match the number of data points"
|
||||||
|
)
|
||||||
|
|
||||||
|
err = err.map(e => {
|
||||||
|
if type(e) in (int, float) { (p: e, m: e) }
|
||||||
|
else if type(e) == dictionary {
|
||||||
|
assert("p" in e and "m" in e, message: "Errorbar dictionaries must contain both \"m\" and \"p\"")
|
||||||
|
e
|
||||||
|
}
|
||||||
|
else { assert(false, message: "Expected a single uncertainty or a dictionary, found " + repr(e))}
|
||||||
|
})
|
||||||
|
err = (p: err.map(e => e.p), m: err.map(e => e.m))
|
||||||
|
|
||||||
|
} else {
|
||||||
|
assert(
|
||||||
|
false,
|
||||||
|
message: "`" + kind + "err` expects a float, an array, or a dictionary with the keys \"p\" and \"m\"."
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
err
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#let render-plot(plot, transform) = {
|
||||||
|
let (points, runs) = filter-nan-points(plot.x.zip(plot.y), generate-runs: true)
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
runs = (((0, 0.5), (1, 0.5)),)
|
||||||
|
points = ((0.5, 0.5),)
|
||||||
|
if plot.yerr != none { plot.yerr = (p: (.5,), m: (.5,),) }
|
||||||
|
if plot.xerr != none { plot.xerr = (p: (.25,), m: (.25,)) }
|
||||||
|
plot.mark.every = none
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let line = merge-strokes(plot.style.stroke, plot.style.color)
|
||||||
|
if line != none {
|
||||||
|
show: prepare-path.with(
|
||||||
|
stroke: line,
|
||||||
|
element: curve
|
||||||
|
)
|
||||||
|
|
||||||
|
let (step, smooth) = (plot.style.step, plot.style.smooth)
|
||||||
|
if step != none and smooth {
|
||||||
|
panic("`step` and `smooth` are mututally exclusive")
|
||||||
|
}
|
||||||
|
|
||||||
|
for run in runs {
|
||||||
|
if run.len() == 0 { continue }
|
||||||
|
|
||||||
|
if step != none { run = stepify(run, step: step )}
|
||||||
|
run = run.map(p => transform(..p))
|
||||||
|
if run.len() > 2 and smooth {
|
||||||
|
let x = run.map(((x, _)) => x)
|
||||||
|
let y = run.map(((_, y)) => y)
|
||||||
|
|
||||||
|
let points = bezier-splines(x, y)
|
||||||
|
|
||||||
|
place(curve(curve.move(points.at(0)), ..points
|
||||||
|
.slice(1)
|
||||||
|
.chunks(3)
|
||||||
|
.map(p => curve.cubic(..p))))
|
||||||
|
} else {
|
||||||
|
place(curve(
|
||||||
|
curve.move(run.first()),
|
||||||
|
..run.slice(1).map(curve.line)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
let errorbar-stroke = prepare-path.with(
|
||||||
|
stroke: merge-strokes(
|
||||||
|
..((dash: "solid"), plot.style.stroke, plot.style.color).filter(x => x != none)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let filter = {
|
||||||
|
let every = plot.mark.every
|
||||||
|
if every == 1 { none }
|
||||||
|
else if type(every) == int {
|
||||||
|
let n = plot.x.len()
|
||||||
|
arr => range(calc.quo(n, every)).map(i => arr.at(i * every))
|
||||||
|
} else if type(every) == dictionary {
|
||||||
|
assertations.assert-dict-keys(every, mandatory: ("n",), optional: ("start", "end"))
|
||||||
|
let size = plot.x.len()
|
||||||
|
let start = every.at("start", default: 0)
|
||||||
|
let end = every.at("end", default: size + 1)
|
||||||
|
if end < 0 { end = size + end }
|
||||||
|
assert(start >= 0 and start < size, message: "Invalid start index " + str(start))
|
||||||
|
|
||||||
|
arr => range(calc.quo(end - start, every.n))
|
||||||
|
.map(i => arr.at(i * every.n + start))
|
||||||
|
} else if type(every) == array {
|
||||||
|
arr => every.map(i => arr.at(i))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if filter != none { points = filter(points) }
|
||||||
|
|
||||||
|
|
||||||
|
if plot.xerr != none {
|
||||||
|
show: errorbar-stroke
|
||||||
|
|
||||||
|
let (p, m) = plot.xerr
|
||||||
|
if filter != none {
|
||||||
|
p = filter(p)
|
||||||
|
m = filter(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
points.zip(p, m).map((((x, y), p, m)) => {
|
||||||
|
let (p0, p1) = (transform(x - m, y), transform(x + p, y))
|
||||||
|
|
||||||
|
place(
|
||||||
|
dx: p0.at(0),
|
||||||
|
dy: p0.at(1),
|
||||||
|
box(width: p1.at(0) - p0.at(0), errorbar(kind: "x"))
|
||||||
|
)
|
||||||
|
|
||||||
|
}).join()
|
||||||
|
}
|
||||||
|
|
||||||
|
if plot.yerr != none {
|
||||||
|
show: errorbar-stroke
|
||||||
|
|
||||||
|
let (p, m) = plot.yerr
|
||||||
|
if filter != none {
|
||||||
|
p = filter(p)
|
||||||
|
m = filter(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
points.zip(p, m).map((((x, y), p, m)) => {
|
||||||
|
let (p0, p1) = (transform(x, y - m), transform(x, y + p))
|
||||||
|
let (y0, y1) = (p0.at(1), p1.at(1)).sorted()
|
||||||
|
|
||||||
|
place(
|
||||||
|
dx: p1.at(0),
|
||||||
|
dy: y0,
|
||||||
|
box(height: y1 - y0, errorbar(kind: "y"))
|
||||||
|
)
|
||||||
|
|
||||||
|
}).join()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.mark.mark,
|
||||||
|
color: plot.style.color,
|
||||||
|
fill: plot.mark.fill,
|
||||||
|
size: plot.mark.size
|
||||||
|
)
|
||||||
|
|
||||||
|
let marker = mark()
|
||||||
|
let transformed-points = points.map(p => transform(..p))
|
||||||
|
transformed-points.map(((x, y)) => place(dx: x, dy: y, marker)).join()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Standard plotting method for 2d data with lines and/or marks and optional
|
||||||
|
/// error bars. Points where the $x$ or $y$ coordinate is `nan` are skipped.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #let x = lq.linspace(0, 10)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(x, x => calc.sin(x + 0.541))
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// The $y$ coordinates can be given either as an array or as a function to be
|
||||||
|
/// evaluated for all $x$ coordinates.
|
||||||
|
///
|
||||||
|
/// By default, the line and mark style is determined by the current
|
||||||
|
/// @diagram.cycle. However, they can be configured per plot with the options
|
||||||
|
/// @plot.color, @plot.mark,
|
||||||
|
/// and @plot.stroke.
|
||||||
|
///
|
||||||
|
/// This function is also intended for creating plots with error bars.
|
||||||
|
/// Error bars can be styled through the @errorbar type.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// range(8), (3, 6, 2, 6, 5, 9, 0, 4),
|
||||||
|
/// yerr: (1, 1, .7, .8, .2, .6, .5, 1),
|
||||||
|
/// stroke: none,
|
||||||
|
/// mark: "star",
|
||||||
|
/// mark-size: 6pt
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let plot(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates. Data coordinates need to be of type `int` or `float`.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Specifies either an array of $y$ coordinates or a function that takes an
|
||||||
|
/// `x` value and returns a corresponding `y` coordinate. The number of $x$
|
||||||
|
/// and $y$ coordinates must match.
|
||||||
|
/// -> array | function
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Optional errors/uncertainties for $x$ coordinates. Symmetric errors can
|
||||||
|
/// be specified as a
|
||||||
|
/// - a constant (e.g., `xerr: 1.5`) or
|
||||||
|
/// - an array with the same length as @plot.x for individual errors per
|
||||||
|
/// data point (e.g., `xerr: (0.5, 1, 1.5)`).
|
||||||
|
///
|
||||||
|
/// Asymmetric errors can be given as
|
||||||
|
/// - a dictionary with the keys `p` (plus) and `m` (minus) with either a
|
||||||
|
/// constant value or arrays with the same length as @plot.x (e.g.,
|
||||||
|
/// `xerr: (p: 1, m: 2)`) or
|
||||||
|
/// - an array of dictionaries per data point, each filled with single `p`
|
||||||
|
/// and `m` values (e.g., `xerr: ((p: 1, m: 2), (p: 2, m: 3))`).
|
||||||
|
///
|
||||||
|
/// The look of the error bars can be controlled through the type @errorbar.
|
||||||
|
/// -> none | array | dictionary
|
||||||
|
xerr: none,
|
||||||
|
|
||||||
|
/// Optional errors/uncertainties for $y$ coordinates. See @plot.xerr.
|
||||||
|
///
|
||||||
|
/// The look of the error bars can be controlled through @errorbar.
|
||||||
|
/// -> none | array
|
||||||
|
yerr: none,
|
||||||
|
|
||||||
|
/// Combined color for line and marks. See also the parameters @plot.stroke and
|
||||||
|
/// @plot.mark-fill which take precedence over `color`, if set.
|
||||||
|
/// -> auto | color
|
||||||
|
color: auto,
|
||||||
|
|
||||||
|
/// The line style to use for this plot (takes precedence over @plot.color).
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// The mark to use to mark data points. This may either be a mark (such as
|
||||||
|
/// `lq.mark.x`) or a registered mark string, see @mark.
|
||||||
|
/// -> auto | none | lq.mark | string
|
||||||
|
mark: auto,
|
||||||
|
|
||||||
|
/// Size of the marks. For variable-size mark plots, use the plot type @scatter.
|
||||||
|
/// -> auto | length
|
||||||
|
mark-size: auto,
|
||||||
|
|
||||||
|
/// Color of the marks (takes precedence over @plot.color).
|
||||||
|
/// TODO: this parameter should eventually be removed. Instead one
|
||||||
|
/// would be able to set mark color and stroke through
|
||||||
|
/// ```
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// plot(..), // normal plot
|
||||||
|
/// {
|
||||||
|
/// set mark(fill: red, stroke: black)
|
||||||
|
/// plot(..)
|
||||||
|
/// }
|
||||||
|
///)
|
||||||
|
/// ```
|
||||||
|
/// This again reduces the API. `mark.size` however is common enough to deserve
|
||||||
|
/// its own parameter.
|
||||||
|
/// -> auto | color
|
||||||
|
mark-color: auto,
|
||||||
|
|
||||||
|
/// Step mode for plotting the lines.
|
||||||
|
/// - `none`: Consecutive data points are connected with a straight line.
|
||||||
|
/// - `start`: The interval $(x_{i-1}, x_i]$ takes the value of $x_i$.
|
||||||
|
/// - `center`: The value switches half-way between consecutive $x$ positions.
|
||||||
|
/// - `end`: The interval $[x_i, x_{i+1})$ takes the value of $x_i$.
|
||||||
|
///
|
||||||
|
/// #details[
|
||||||
|
/// ```example
|
||||||
|
/// #import lilaq
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// range(8), (3,6,2,6,5,9,0,4),
|
||||||
|
/// step: center
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> none | start | end | center
|
||||||
|
step: none,
|
||||||
|
|
||||||
|
/// Interpolates the data set using Bézier splines instead of connecting the points with straight lines.
|
||||||
|
///
|
||||||
|
/// Note: If two or fewer points are given, linear interpolation is used.
|
||||||
|
///
|
||||||
|
/// #details[
|
||||||
|
/// ```example
|
||||||
|
/// #import lilaq
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// range(8), (3, 6, 2, 6, 5, 9, 0, 4),
|
||||||
|
/// smooth: true
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
///
|
||||||
|
/// -> bool
|
||||||
|
smooth: false,
|
||||||
|
|
||||||
|
/// Specifies the interval of marks to plot. This can be used to skip
|
||||||
|
/// marks while still drawing lines between all points.
|
||||||
|
/// - `none`: All marks are plotted.
|
||||||
|
/// - `int`: Every $n$-th mark is plotted.
|
||||||
|
/// - `array`: All marks with the given indices are plotted.
|
||||||
|
/// - `dict`: A dictionary with the keys `n`, `start` (start index), and
|
||||||
|
/// `end` (end index, negative indices count from the end) can be used
|
||||||
|
/// to specify a range of marks to plot.
|
||||||
|
///
|
||||||
|
/// #details[
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.plot(
|
||||||
|
/// range(20),
|
||||||
|
/// range(20).map(x => x*x),
|
||||||
|
/// every: 4
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> none | int | array | dict
|
||||||
|
every: none,
|
||||||
|
|
||||||
|
/// The legend label for this plot. If not given, the plot will not appear in the
|
||||||
|
/// legend.
|
||||||
|
/// -> any
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. This is usually a good idea for plots
|
||||||
|
/// with lines but it does also clip part of marks that lie right on an axis.
|
||||||
|
/// #details[
|
||||||
|
/// Comparison between clipped and non-clipped plot.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// margin: 0%,
|
||||||
|
/// lq.plot(
|
||||||
|
/// (1, 2, 3), (2.5, 1.9, 1.5),
|
||||||
|
/// mark: "o",
|
||||||
|
/// ),
|
||||||
|
/// lq.plot(
|
||||||
|
/// (1, 2, 3), (1, 2.1, 3),
|
||||||
|
/// mark: "o",
|
||||||
|
/// clip: false
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Specifies the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. This makes it also possible to render plots in front of the axes
|
||||||
|
/// which have a z-index of `20`.
|
||||||
|
/// #details[
|
||||||
|
/// In this example, the points are listed before the bars in the legend but
|
||||||
|
/// they are still drawn in front of the bars.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// legend: (position: bottom),
|
||||||
|
/// lq.plot(
|
||||||
|
/// (1, 2, 3, 4), (2, 3, 4, 5),
|
||||||
|
/// mark-size: 10pt,
|
||||||
|
/// z-index: 2.01,
|
||||||
|
/// label: [Points],
|
||||||
|
/// ),
|
||||||
|
/// lq.bar(
|
||||||
|
/// (1, 2, 3, 4), (2, 3, 4, 5),
|
||||||
|
/// label: [Bars],
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(y) == function {
|
||||||
|
y = x.map(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(x, y, fn-name: "plot")
|
||||||
|
|
||||||
|
assert(step in (none, start, end, center))
|
||||||
|
|
||||||
|
if xerr != none { xerr = process-errors(xerr, x.len(), kind: "x") }
|
||||||
|
if yerr != none { yerr = process-errors(yerr, x.len(), kind: "y") }
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
xerr: xerr,
|
||||||
|
yerr: yerr,
|
||||||
|
label: label,
|
||||||
|
mark: (
|
||||||
|
mark: mark,
|
||||||
|
fill: mark-color,
|
||||||
|
size: mark-size,
|
||||||
|
every: every
|
||||||
|
),
|
||||||
|
style: (
|
||||||
|
stroke: stroke,
|
||||||
|
color: color,
|
||||||
|
step: step,
|
||||||
|
smooth: smooth
|
||||||
|
),
|
||||||
|
plot: render-plot,
|
||||||
|
xlimits: () => plot-lim(x, err: xerr),
|
||||||
|
ylimits: () => plot-lim(y, err: yerr),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
303
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/quiver.typ
vendored
Normal file
303
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/quiver.typ
vendored
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/sample-colors.typ": sample-colors
|
||||||
|
#import "../process-styles.typ": merge-strokes
|
||||||
|
#import "../math.typ": vec, mesh
|
||||||
|
#import "../utility.typ": if-auto, match, match-type
|
||||||
|
#import "../style/styling.typ": style
|
||||||
|
|
||||||
|
#import "@preview/tiptoe:0.3.1"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-quiver(plot, transform) = context {
|
||||||
|
|
||||||
|
|
||||||
|
let arrow = plot.style.arrow
|
||||||
|
let get-arrow-stroke = plot.style.get-arrow-stroke
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return arrow(
|
||||||
|
length: 100%,
|
||||||
|
stroke: get-arrow-stroke(0, 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let pivot = match(plot.style.pivot,
|
||||||
|
center, () => dir => dir.map(x => -x*0.5),
|
||||||
|
start, () => dir => (0, 0),
|
||||||
|
end, () => dir => dir.map(x => -x),
|
||||||
|
default: () => assert(false, message: "The argument `pivot` of `quiver` needs to be one of 'start', 'center', or 'end'")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for i in range(plot.x.len()) {
|
||||||
|
for j in range(plot.y.len()) {
|
||||||
|
let dir = plot.directions.at(j).at(i)
|
||||||
|
if dir.any(float.is-nan) { continue }
|
||||||
|
|
||||||
|
let x = plot.x.at(i)
|
||||||
|
let y = plot.y.at(j)
|
||||||
|
dir = vec.multiply(dir, plot.scale)
|
||||||
|
let length = calc.sqrt(dir.map(x => x*x).sum())
|
||||||
|
let start = vec.add((x, y), pivot(dir))
|
||||||
|
let end = vec.add(start, dir)
|
||||||
|
|
||||||
|
let stroke = get-arrow-stroke(i + j*plot.x.len(), length)
|
||||||
|
|
||||||
|
if length == 0 {
|
||||||
|
let (x, y) = transform(..start)
|
||||||
|
let radius = plot.style.thickness / 2
|
||||||
|
place(
|
||||||
|
dx: x - radius, dy: y - radius,
|
||||||
|
circle(radius: radius, fill: stroke.paint)
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
place(
|
||||||
|
arrow(
|
||||||
|
start: transform(..start),
|
||||||
|
end: transform(..end),
|
||||||
|
stroke: stroke
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a quiver plot for visualizing vector fields over a rectangular
|
||||||
|
/// coordinate grid.
|
||||||
|
///
|
||||||
|
/// The `quiver` function takes an array of $x$- and $y$-coordinates as well as
|
||||||
|
/// a two-dimensional array of vector directions or a mapper from `(x, y)`
|
||||||
|
/// pairs to direction vectors.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.quiver(
|
||||||
|
/// lq.arange(-2, 3),
|
||||||
|
/// lq.arange(-2, 3),
|
||||||
|
/// (x, y) => (x + y, y - x)
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// By default, arrow lengths and strokes are scaled automatically to
|
||||||
|
/// compensate for the density of a quiver plot.
|
||||||
|
///
|
||||||
|
/// On top, the arrows can easily be color-coded, similar to @colormesh,
|
||||||
|
/// see @quiver.color.
|
||||||
|
#let quiver(
|
||||||
|
|
||||||
|
/// A one-dimensional array of $x$ data coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// A one-dimensional array of $y$ data coordinates.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Direction vectors for the arrows.
|
||||||
|
/// This can either be
|
||||||
|
/// - a two-dimensional array of dimensions $m×n$ where $m$ is the length
|
||||||
|
/// of @quiver.x and $n$ is the length of @quiver.y,
|
||||||
|
/// - or a function that takes two arguments `(x, y)` and returns a
|
||||||
|
/// two-dimensional direction vector.
|
||||||
|
///
|
||||||
|
/// Masking is possible through `nan` values.
|
||||||
|
/// -> array | function
|
||||||
|
directions,
|
||||||
|
|
||||||
|
/// How to stroke the arrows. This parameter takes precedence over
|
||||||
|
/// @quiver.color. If the stroke thickness is left at `auto`, small
|
||||||
|
/// arrows will be drawn with a thinner line style.
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// Scales the length of the arrows uniformly. If set to `auto`, the length
|
||||||
|
/// is heuristically computed from the density of the coordinate grid.
|
||||||
|
/// -> auto | int | float
|
||||||
|
scale: auto,
|
||||||
|
|
||||||
|
/// With which part the arrows should point onto the grid coordinates, e.g.,
|
||||||
|
/// when set to `end`, the tip (end) of the arrow will point to the
|
||||||
|
/// respective coordinates.
|
||||||
|
///
|
||||||
|
/// #details[
|
||||||
|
/// ```example
|
||||||
|
/// #show: lq.set-diagram(
|
||||||
|
/// xaxis: (tick-distance: 1),
|
||||||
|
/// xlim: (-3, 3),
|
||||||
|
/// ylim: (-3, 3),
|
||||||
|
/// width: 4cm
|
||||||
|
/// )
|
||||||
|
///
|
||||||
|
/// #let x = lq.arange(-2, 3)
|
||||||
|
/// #let y = lq.arange(-2, 3)
|
||||||
|
/// #let directions = lq.mesh(x, y, (x, y) => (x + y, y - x))
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// title: [`pivot: end`],
|
||||||
|
/// lq.quiver(x, y, directions, pivot: end),
|
||||||
|
/// )
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// title: [`pivot: center` (default)],
|
||||||
|
/// lq.quiver(x, y, directions, pivot: center),
|
||||||
|
/// )
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// title: [`pivot: start`],
|
||||||
|
/// lq.quiver(x, y, directions, pivot: start),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> start | center | end
|
||||||
|
pivot: end,
|
||||||
|
|
||||||
|
/// Determines the arrow tip to use. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
tip: tiptoe.stealth.with(length: 400%),
|
||||||
|
|
||||||
|
/// Determines the arrow tail to use. This expects a mark as specified by
|
||||||
|
/// the #link("https://typst.app/universe/package/tiptoe")[tiptoe package].
|
||||||
|
/// -> none | tiptoe.mark
|
||||||
|
toe: none,
|
||||||
|
|
||||||
|
/// How to color the arrows. This can be a single color or a two-dimensional
|
||||||
|
/// array with the same dimensions as @quiver or a function that receives
|
||||||
|
/// `(x, y, u, v)` tuples of `x` and `y` coordinates and corresponding
|
||||||
|
/// direction components and returns a scalar (`float`) or `color`.
|
||||||
|
///
|
||||||
|
/// #details[
|
||||||
|
/// In this example, we make a color-coding by taking the length of the
|
||||||
|
/// direction vectors.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.quiver(
|
||||||
|
/// lq.arange(-2, 3),
|
||||||
|
/// lq.arange(-2, 3),
|
||||||
|
/// (x, y) => (x + 2 * y, y - x),
|
||||||
|
/// color: (x, y, u, v) => calc.norm(u, v),
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
/// ]
|
||||||
|
/// -> color | array | function
|
||||||
|
color: black,
|
||||||
|
|
||||||
|
/// A color map to sample from. The color map can be given in form of a
|
||||||
|
/// gradient or an array of colors.
|
||||||
|
/// -> array | gradient
|
||||||
|
map: color.map.viridis,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the first color of the color map.
|
||||||
|
/// If set to `auto`, it defaults to the minimum color value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the last color of the color map.
|
||||||
|
/// If set to `auto`, it defaults to the maximum color value.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// The normalization method used to scale @quiver.color scalars to the range
|
||||||
|
/// $[0,1]$ before mapping them to colors using the color map. This can be a
|
||||||
|
/// @scale, a string that is the identifier of a built-in scale or a function
|
||||||
|
/// that takes one argument.
|
||||||
|
/// -> lq.scale | str | function
|
||||||
|
norm: "linear",
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2
|
||||||
|
|
||||||
|
) = {
|
||||||
|
|
||||||
|
if type(directions) == function {
|
||||||
|
directions = mesh(x, y, directions)
|
||||||
|
}
|
||||||
|
|
||||||
|
if type(color) == function {
|
||||||
|
color = mesh(
|
||||||
|
x.enumerate(), y.enumerate(),
|
||||||
|
((i, x), (j, y)) => color(x, y, ..directions.at(j).at(i))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
let cinfo
|
||||||
|
|
||||||
|
if type(color) == array {
|
||||||
|
assert(type(color.first()) == array, message: "The argument `color` for `quiver` either needs to be a single value or a 2D array")
|
||||||
|
let color-flat = color.flatten()
|
||||||
|
assert(x.len() * y.len() == color-flat.len(), message: "The number of elements in `color` does not match the grid for `quiver()`")
|
||||||
|
|
||||||
|
if type(color-flat.at(0, default: 0)) in (int, float) {
|
||||||
|
(color, cinfo) = sample-colors(color-flat, map, norm, ignore-nan: true, min: min, max: max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if scale == auto {
|
||||||
|
let lengths = directions.join().map(a => calc.sqrt(a.map(b => b*b).sum()))
|
||||||
|
lengths = lengths.filter(x => not float.is-nan(x))
|
||||||
|
|
||||||
|
let n = lengths.len()
|
||||||
|
let average = lengths.sum() / n
|
||||||
|
let compensation = calc.max(4., calc.pow(n, .6) * .6)
|
||||||
|
|
||||||
|
scale = 1 / (0.54 * average * compensation)
|
||||||
|
}
|
||||||
|
|
||||||
|
if stroke != auto { stroke = std.stroke(stroke) }
|
||||||
|
|
||||||
|
let thickness = match-type(
|
||||||
|
stroke,
|
||||||
|
stroke: () => if-auto(stroke.thickness, 1pt),
|
||||||
|
auto-type: 1pt
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
let get-stroke = {
|
||||||
|
if stroke != auto and stroke.thickness != auto {
|
||||||
|
let p = stroke
|
||||||
|
(color, length) => merge-strokes(stroke, color)
|
||||||
|
} else {
|
||||||
|
(color, length) => merge-strokes(
|
||||||
|
1pt*calc.clamp(4 * length, .2, 1),
|
||||||
|
stroke, color
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
let get-arrow-stroke = match-type(
|
||||||
|
color,
|
||||||
|
panic: true,
|
||||||
|
color: () => (i, len) => get-stroke(color, len),
|
||||||
|
array: () => (i, len) => get-stroke(color.at(i), len),
|
||||||
|
)
|
||||||
|
let arrow = tiptoe.line.with(tip: tip, toe: toe)
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
cinfo: cinfo,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
directions: directions,
|
||||||
|
scale: scale,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
thickness: thickness,
|
||||||
|
pivot: pivot,
|
||||||
|
color: color,
|
||||||
|
arrow: arrow,
|
||||||
|
get-arrow-stroke: get-arrow-stroke
|
||||||
|
),
|
||||||
|
plot: render-quiver,
|
||||||
|
xlimits: () => (x.at(0), x.at(-1)),
|
||||||
|
ylimits: () => (y.at(0), y.at(-1)),
|
||||||
|
legend: true,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
130
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/rect.typ
vendored
Normal file
130
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/rect.typ
vendored
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": compute-primitive-limits
|
||||||
|
#import "../logic/process-coordinates.typ": all-data-coordinates, convert-rect
|
||||||
|
#import "../process-styles.typ": twod-ify-alignment
|
||||||
|
|
||||||
|
|
||||||
|
/// Plots a rectangle or square with origin `(x, y)`. The origin coordinates as well
|
||||||
|
/// as width and height can either be given as
|
||||||
|
/// - data coordinates (`int` or `float`),
|
||||||
|
/// - or absolute coordinates from the top left corner of the data area (`length`),
|
||||||
|
/// - or in percent relative to the data area (`ratio`),
|
||||||
|
/// - or a combination of the latter two (`relative`).
|
||||||
|
///
|
||||||
|
/// Note that coordinate types can also be mixed (e.g., a length for `x` and a scalar for `y`).
|
||||||
|
///
|
||||||
|
/// For example in order to access the center, you can write `(50%, 50%)`.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// width: 3cm, height: 3cm,
|
||||||
|
/// lq.rect(2, 2, width: 10, height: 4, fill: yellow),
|
||||||
|
/// lq.rect(10, 4, width: 4, height: 4, fill: red),
|
||||||
|
/// lq.rect(50%, 50%, width: 45%, height: 45%, stroke: blue)
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
#let rect(
|
||||||
|
|
||||||
|
/// The x coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// The y coordinate of the origin.
|
||||||
|
/// -> float | relative
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// The rectangle's width.
|
||||||
|
/// -> auto | float | relative
|
||||||
|
width: auto,
|
||||||
|
|
||||||
|
/// The rectangle's height.
|
||||||
|
/// -> auto | float | relative
|
||||||
|
height: auto,
|
||||||
|
|
||||||
|
/// How to align the rectangle at the origin.
|
||||||
|
/// -> alignment
|
||||||
|
align: left + top,
|
||||||
|
|
||||||
|
/// How to fill the rectangle.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
fill: none,
|
||||||
|
|
||||||
|
/// How to stroke the rectangle.
|
||||||
|
/// -> auto | none | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// The corner rounding radius of the rectangle.
|
||||||
|
/// -> relative
|
||||||
|
radius: 0pt,
|
||||||
|
|
||||||
|
/// How much to pad the content of the rectangle. See the built-in [`std.rect#inset`](https://typst.app/docs/reference/visualize/rect/#parameters-inset).
|
||||||
|
/// -> relative | dictionary
|
||||||
|
inset: 5pt,
|
||||||
|
|
||||||
|
/// How much to expand the rectangle beyond its defined size.
|
||||||
|
/// See the built-in [`std.rect#outset`](https://typst.app/docs/reference/visualize/rect/#parameters-outset).
|
||||||
|
/// -> relative | dictionary
|
||||||
|
outset: 0pt,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram objects.
|
||||||
|
/// See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
/// An optional body to place inside the rectangle.
|
||||||
|
/// -> any
|
||||||
|
..body
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(body, fn: "rect")
|
||||||
|
|
||||||
|
let body = body.pos().at(0, default: none)
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
plot: (plot, transform) => {
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return std.rect(
|
||||||
|
width: 100%, height: 100%,
|
||||||
|
fill: fill, stroke: stroke, radius: radius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x1, width, y1, height) = convert-rect(
|
||||||
|
x, y,
|
||||||
|
width, height,
|
||||||
|
transform,
|
||||||
|
align: twod-ify-alignment(align)
|
||||||
|
)
|
||||||
|
|
||||||
|
place(dx: x1, dy: y1,
|
||||||
|
std.rect(
|
||||||
|
width: width,
|
||||||
|
height: height,
|
||||||
|
fill: fill,
|
||||||
|
stroke: stroke,
|
||||||
|
radius: radius,
|
||||||
|
inset: inset,
|
||||||
|
outset: outset,
|
||||||
|
body
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
xlimits: compute-primitive-limits.with((x, if all-data-coordinates((x, width)) { x + width } else { x })),
|
||||||
|
ylimits: compute-primitive-limits.with((y, if all-data-coordinates((y, height)) { y + height } else { y })),
|
||||||
|
label: label,
|
||||||
|
legend: true,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
226
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/scatter.typ
vendored
Normal file
226
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/scatter.typ
vendored
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
#import "../process-styles.typ": merge-strokes
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points
|
||||||
|
#import "../logic/sample-colors.typ": sample-colors
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark, _auto, style
|
||||||
|
#import "../utility.typ": if-auto, match-type, if-none
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-scatter(plot, transform) = {
|
||||||
|
|
||||||
|
let get-mark-size = match-type(
|
||||||
|
plot.size,
|
||||||
|
length: () => i => plot.size,
|
||||||
|
array: () => i => plot.size.at(i),
|
||||||
|
auto-type: () => i => auto,
|
||||||
|
panic: true
|
||||||
|
)
|
||||||
|
|
||||||
|
let get-mark-color = match-type(
|
||||||
|
plot.color,
|
||||||
|
color: () => i => plot.color,
|
||||||
|
array: () => i => plot.color.at(i),
|
||||||
|
auto-type: () => i => _auto,
|
||||||
|
panic: true,
|
||||||
|
)
|
||||||
|
let get-alpha = match-type(
|
||||||
|
plot.alpha,
|
||||||
|
ratio: () => i => plot.alpha,
|
||||||
|
array: () => i => plot.alpha.at(i),
|
||||||
|
panic: true,
|
||||||
|
)
|
||||||
|
|
||||||
|
let points = filter-nan-points(plot.x.zip(plot.y))
|
||||||
|
if "make-legend" in plot {
|
||||||
|
points = ((0.5,.5),)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mark,
|
||||||
|
color: if type(plot.color) == array { _auto } else { plot.color },
|
||||||
|
stroke: plot.style.stroke
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
for (i, p) in points.enumerate() {
|
||||||
|
let p = transform(..p)
|
||||||
|
|
||||||
|
let size = get-mark-size(i)
|
||||||
|
let mark-color = get-mark-color(i)
|
||||||
|
let mark-fill-color = mark-color.transparentize(100% - get-alpha(i))
|
||||||
|
|
||||||
|
let options = (:)
|
||||||
|
if mark-fill-color != _auto {
|
||||||
|
options.fill = mark-fill-color
|
||||||
|
}
|
||||||
|
if size != auto {
|
||||||
|
options.inset = size
|
||||||
|
}
|
||||||
|
|
||||||
|
place(dx: p.at(0), dy: p.at(1), mark(..options))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Produces a scatter plot from the given coordinates. The mark size and
|
||||||
|
/// color can be set per point (this discerns @scatter from @plot).
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #import "@preview/suiji:0.3.0"
|
||||||
|
/// #let rng = suiji.gen-rng(33)
|
||||||
|
/// #let (rng, x) = suiji.uniform(rng, size: 20)
|
||||||
|
/// #let (rng, y) = suiji.uniform(rng, size: 20)
|
||||||
|
/// #let (rng, colors) = suiji.uniform(rng, size: 20)
|
||||||
|
/// #let (rng, sizes) = suiji.uniform(rng, size: 20)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.scatter(
|
||||||
|
/// x, y,
|
||||||
|
/// size: sizes.map(size => 200 * size),
|
||||||
|
/// color: colors,
|
||||||
|
/// map: color.map.magma
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let scatter(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// An array of $y$ coordinates. The number of $x$ and $y$ coordinates must match.
|
||||||
|
/// -> array
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// How to scale the marks. The area of the marks scales proportionally
|
||||||
|
/// with these numbers while the actual mark "size" (e.g., width) will scale with
|
||||||
|
/// $\sqrt{\mathrm{size}}$.
|
||||||
|
/// -> auto | array
|
||||||
|
size: auto,
|
||||||
|
|
||||||
|
/// How to fill the marks. A single color can be used to override the default
|
||||||
|
/// cycle fill style.
|
||||||
|
///
|
||||||
|
/// When given per-point as an array, the values may either be of type
|
||||||
|
/// - `color` (then the @scatter.map is ignored)
|
||||||
|
/// - or of type `float` in which case the colors are computed by normalizing
|
||||||
|
/// the given values and passing them through @scatter.map.
|
||||||
|
/// -> auto | color | array
|
||||||
|
color: auto,
|
||||||
|
|
||||||
|
/// A color map in the form of a gradient or an array of colors to sample from when
|
||||||
|
/// @scatter.color is given as a `float` array.
|
||||||
|
/// -> array | gradient
|
||||||
|
map: color.map.viridis,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the first color of the color map. If set
|
||||||
|
/// to `auto`, it defaults to the minimum value of @scatter.color.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// Sets the data value that corresponds to the last color of the color map. If set
|
||||||
|
/// to `auto`, it defaults to the maximum value of @scatter.color.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// The normalization method used to scale `float` @scatter.color values to
|
||||||
|
/// the range $[0,1]$ before mapping them to colors with the color map. This
|
||||||
|
/// can be
|
||||||
|
/// - a @scale,
|
||||||
|
/// - a string that is the identifier of a built-in scale, or
|
||||||
|
/// - a function that takes one argument (for example `x => calc.log(x)`
|
||||||
|
/// would be equivalent to passing `"log"`). Note that the function does not
|
||||||
|
/// actually need to map the values to the interval $[0,1]$. Instead it
|
||||||
|
/// describes a scaling that is applied before the data set is _linearly_
|
||||||
|
/// scaled to the interval $[0,1]$.
|
||||||
|
/// -> lq.scale | str | function
|
||||||
|
norm: "linear",
|
||||||
|
|
||||||
|
/// The mark to use to mark data points. See @plot.mark.
|
||||||
|
/// -> auto | lq.mark | string
|
||||||
|
mark: auto,
|
||||||
|
|
||||||
|
/// Mark stroke. TODO: need to get rid of it
|
||||||
|
/// -> stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// The fill opacity. TODO: maybe get rid of it, there is already color.
|
||||||
|
/// -> ratio | array
|
||||||
|
alpha: 100%,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(x, y, fn-name: "scatter")
|
||||||
|
|
||||||
|
|
||||||
|
if type(size) == array {
|
||||||
|
assert.eq(x.len(), size.len(), message: "Input dimensions for x (" + str(x.len()) + ") and size (" + str(size.len()) + ") don't match")
|
||||||
|
let size-type = type(size.at(0, default: 0))
|
||||||
|
if size-type in (int, float) {
|
||||||
|
size = size.map(x => calc.sqrt(x) * 1pt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let cinfo
|
||||||
|
if type(color) == array {
|
||||||
|
assert.eq(x.len(), color.len(), message: "Input dimensions for coordinates (" + str(x.len()) + ") and color (" + str(color.len()) + ") don't match")
|
||||||
|
|
||||||
|
if type(color.at(0, default: 0)) in (int, float) {
|
||||||
|
if type(map) == array {
|
||||||
|
map = gradient.linear(..map)
|
||||||
|
}
|
||||||
|
(color, cinfo) = sample-colors(color, map, norm, ignore-nan: false, min: min, max: max)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
cinfo: cinfo,
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
size: size,
|
||||||
|
color: color,
|
||||||
|
alpha: alpha,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
mark: mark,
|
||||||
|
stroke: stroke,
|
||||||
|
map: map,
|
||||||
|
),
|
||||||
|
plot: render-scatter,
|
||||||
|
xlimits: () => minmax(x),
|
||||||
|
ylimits: () => minmax(y),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
173
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/stem.typ
vendored
Normal file
173
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/stem.typ
vendored
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/limits.typ": bar-lim
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
#import "../process-styles.typ": merge-strokes, merge-fills
|
||||||
|
#import "../logic/process-coordinates.typ": filter-nan-points
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../utility.typ": if-auto
|
||||||
|
#import "../style/styling.typ": mark, prepare-mark, prepare-line
|
||||||
|
|
||||||
|
#let render-stem(plot, transform) = {
|
||||||
|
let marker = plot.style.mark
|
||||||
|
let marker = mark()
|
||||||
|
|
||||||
|
|
||||||
|
let (xmin, xmax) = (plot.xlimits)()
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
xmin = 0
|
||||||
|
xmax = 1
|
||||||
|
plot.x = (.5,)
|
||||||
|
plot.y = (.8,)
|
||||||
|
plot.base = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
let (x1, y0) = transform(xmin, plot.base)
|
||||||
|
let (x2, y0) = transform(xmax, plot.base)
|
||||||
|
let points = filter-nan-points(plot.x.zip(plot.y)).map(p => transform(..p))
|
||||||
|
|
||||||
|
show: prepare-mark.with(
|
||||||
|
func: plot.style.mark,
|
||||||
|
color: merge-fills(plot.style.color),
|
||||||
|
size: plot.style.mark-size
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
{
|
||||||
|
|
||||||
|
|
||||||
|
show: prepare-line.with(
|
||||||
|
stroke: merge-strokes(plot.style.stroke, plot.style.color)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
points.map(((x, y)) => place(line(start: (x, y0), end: (x, y)))).join()
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if plot.style.base-stroke != none {
|
||||||
|
set line(stroke: (cap: "square"))
|
||||||
|
place(line(
|
||||||
|
start: (x1, y0),
|
||||||
|
end: (x2, y0),
|
||||||
|
stroke: plot.style.base-stroke
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
points.map(((x, y)) => place(dx: x, dy: y, marker)).join()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Creates a vertical stem plot.
|
||||||
|
///
|
||||||
|
/// ```example
|
||||||
|
/// #let x = lq.linspace(0, 10, num: 30)
|
||||||
|
///
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// lq.stem(
|
||||||
|
/// x,
|
||||||
|
/// x.map(calc.cos),
|
||||||
|
/// color: orange,
|
||||||
|
/// mark: "d",
|
||||||
|
/// base: -0.25,
|
||||||
|
/// base-stroke: black,
|
||||||
|
/// )
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Also see @hstem for horizontal stem plots.
|
||||||
|
#let stem(
|
||||||
|
|
||||||
|
/// An array of $x$ coordinates.
|
||||||
|
/// -> array
|
||||||
|
x,
|
||||||
|
|
||||||
|
/// Specifies either an array of $y$ coordinates or a function that takes an
|
||||||
|
/// `x` value and returns a corresponding `y` coordinate. The number of $x$
|
||||||
|
/// and $y$ coordinates must match.
|
||||||
|
/// -> array | function
|
||||||
|
y,
|
||||||
|
|
||||||
|
/// Combined color for line and marks. See also the parameter @stem.line
|
||||||
|
/// which takes precedence over `color`, if set.
|
||||||
|
/// -> auto | color
|
||||||
|
color: auto,
|
||||||
|
|
||||||
|
/// The line style to use for this plot (takes precedence over @stem.color).
|
||||||
|
/// -> auto | stroke
|
||||||
|
stroke: auto,
|
||||||
|
|
||||||
|
/// The mark to use to mark data points. See @plot.mark.
|
||||||
|
/// -> auto | none | lq.mark | str
|
||||||
|
mark: auto,
|
||||||
|
|
||||||
|
/// Size of the marks.
|
||||||
|
/// -> auto | length
|
||||||
|
mark-size: auto,
|
||||||
|
|
||||||
|
/// Defines the $y$ coordinate of the base line.
|
||||||
|
/// -> int | float
|
||||||
|
base: 0,
|
||||||
|
|
||||||
|
/// How to stroke the base line.
|
||||||
|
/// -> stroke
|
||||||
|
base-stroke: red,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Whether to clip the plot to the data area. See @plot.clip.
|
||||||
|
/// -> bool
|
||||||
|
clip: true,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(y) == function {
|
||||||
|
y = x.map(y)
|
||||||
|
}
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
if type(y.at(0, default: 0)) == datetime {
|
||||||
|
y = time.to-seconds(..y)
|
||||||
|
datetime-axes.y = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assertations.assert-matching-data-dimensions(x, y, fn-name: "stem")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
y: y,
|
||||||
|
base: base,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
mark: mark,
|
||||||
|
color: color,
|
||||||
|
mark-size: mark-size,
|
||||||
|
stroke: merge-strokes(stroke, color),
|
||||||
|
base-stroke: base-stroke
|
||||||
|
),
|
||||||
|
plot: render-stem,
|
||||||
|
xlimits: () => minmax(x),
|
||||||
|
ylimits: () => bar-lim(y, (base,)),
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
ignores-cycle: false,
|
||||||
|
clip: clip,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
106
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/vlines.typ
vendored
Normal file
106
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/plot/vlines.typ
vendored
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
|
||||||
|
#import "../process-styles.typ": merge-strokes
|
||||||
|
#import "../math.typ": minmax
|
||||||
|
#import "../assertations.typ"
|
||||||
|
#import "../logic/time.typ"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#let render-vlines(plot, transform) = {
|
||||||
|
let min = plot.min
|
||||||
|
let max = plot.max
|
||||||
|
|
||||||
|
if "make-legend" in plot {
|
||||||
|
return line(length: 100%, stroke: plot.style.stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
if min == auto { min = 100% }
|
||||||
|
else {
|
||||||
|
(_, min) = transform(1, min)
|
||||||
|
}
|
||||||
|
if max == auto { max = 0% }
|
||||||
|
else {
|
||||||
|
(_, max) = transform(1, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
for x in plot.x {
|
||||||
|
let (xx, _) = transform(x, 1)
|
||||||
|
place(line(start: (xx, min), end: (xx, max), stroke: plot.style.stroke))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Draws a set of vertical lines into the diagram.
|
||||||
|
/// ```example
|
||||||
|
/// #lq.diagram(
|
||||||
|
/// ylim: (0, 7),
|
||||||
|
/// lq.vlines(1, 1.1, stroke: teal, label: "Indefinite"),
|
||||||
|
/// lq.vlines(2, stroke: blue, min: 2, label: "Fixed start"),
|
||||||
|
/// lq.vlines(3, stroke: purple, max: 2, label: "Fixed end"),
|
||||||
|
/// lq.vlines(4, stroke: red, min: 1, max: 3, label: "Fixed"),
|
||||||
|
/// )
|
||||||
|
/// ```
|
||||||
|
#let vlines(
|
||||||
|
|
||||||
|
/// The $x$ coordinate(s) of one or more vertical lines to draw.
|
||||||
|
/// -> int | float
|
||||||
|
..x,
|
||||||
|
|
||||||
|
/// The beginning of the line as $y$ coordinate. If set to `auto`, the line will
|
||||||
|
/// always start at the bottom of the diagram.
|
||||||
|
/// -> auto | int | float
|
||||||
|
min: auto,
|
||||||
|
|
||||||
|
/// The end of the line as $y$ coordinate. If set to `auto`, the line will
|
||||||
|
/// always end at the top of the diagram.
|
||||||
|
/// -> auto | int | float
|
||||||
|
max: auto,
|
||||||
|
|
||||||
|
/// How to stroke the lines.
|
||||||
|
/// -> stroke
|
||||||
|
stroke: black,
|
||||||
|
|
||||||
|
/// The legend label for this plot. See @plot.label.
|
||||||
|
/// -> content
|
||||||
|
label: none,
|
||||||
|
|
||||||
|
/// Determines the $z$ position of this plot in the order of rendered diagram
|
||||||
|
/// objects. See @plot.z-index.
|
||||||
|
/// -> int | float
|
||||||
|
z-index: 2,
|
||||||
|
|
||||||
|
) = {
|
||||||
|
assertations.assert-no-named(x)
|
||||||
|
x = x.pos()
|
||||||
|
|
||||||
|
let datetime-axes = (:)
|
||||||
|
if type(x.at(0, default: 0)) == datetime {
|
||||||
|
x = time.to-seconds(..x)
|
||||||
|
datetime-axes.x = true
|
||||||
|
}
|
||||||
|
|
||||||
|
let ylimits = none
|
||||||
|
if min != auto {
|
||||||
|
ylimits = (min, min)
|
||||||
|
}
|
||||||
|
if max != auto {
|
||||||
|
ylimits = (if min == auto { max } else { min }, max)
|
||||||
|
}
|
||||||
|
|
||||||
|
(
|
||||||
|
x: x,
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
label: label,
|
||||||
|
style: (
|
||||||
|
stroke: stroke,
|
||||||
|
),
|
||||||
|
plot: render-vlines,
|
||||||
|
xlimits: () => minmax(x),
|
||||||
|
ylimits: () => ylimits,
|
||||||
|
datetime: datetime-axes,
|
||||||
|
legend: true,
|
||||||
|
z-index: z-index
|
||||||
|
)
|
||||||
|
}
|
||||||
158
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/process-styles.typ
vendored
Normal file
158
.obsidian/plugins/typst-mate/packages/preview/lilaq/0.5.0/src/process-styles.typ
vendored
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
/// Updates the first stroke with the second, i.e., returns the second stroke but all
|
||||||
|
/// fields that are auto are inherited from the first stroke.
|
||||||
|
/// If the second stroke is none, returns none.
|
||||||
|
#let update-stroke(stroke1, stroke2) = {
|
||||||
|
let if-not-auto(a, b) = if b == auto {a} else {b}
|
||||||
|
if stroke2 == none { return none }
|
||||||
|
if stroke1 == none { stroke1 = stroke() }
|
||||||
|
let s1 = stroke(stroke1)
|
||||||
|
let s2 = stroke(stroke2)
|
||||||
|
let paint = if-not-auto(s1.paint, s2.paint)
|
||||||
|
let thickness = if-not-auto(s1.thickness, s2.thickness)
|
||||||
|
let cap = if-not-auto(s1.cap, s2.cap)
|
||||||
|
let join = if-not-auto(s1.join, s2.join)
|
||||||
|
let dash = if-not-auto(s1.dash, s2.dash)
|
||||||
|
let miter-limit = if-not-auto(s1.miter-limit, s2.miter-limit)
|
||||||
|
return stroke(paint: paint, thickness: thickness, cap: cap, join: join, dash: dash, miter-limit: miter-limit)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Merge a series of fills sorted by descending priority. Fills exclude each other,
|
||||||
|
/// so the first fill that is not auto is returned. If all fills are
|
||||||
|
/// `auto`, then `auto` is returned.
|
||||||
|
///
|
||||||
|
/// -> auto | none | color | gradient | tiling
|
||||||
|
#let merge-fills(
|
||||||
|
|
||||||
|
/// The fills to merge.
|
||||||
|
/// -> none | color | gradient | tiling
|
||||||
|
..fills
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let fills = fills.pos()
|
||||||
|
let pos = fills.position(x => x != auto)
|
||||||
|
if pos == none { return auto }
|
||||||
|
return fills.at(pos)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Merge a series of strokes sorted by descending priority.
|
||||||
|
///
|
||||||
|
/// -> stroke
|
||||||
|
#let merge-strokes(
|
||||||
|
|
||||||
|
/// The strokes to merge.
|
||||||
|
/// -> none | auto | stroke | length | color | gradient | tiling | dictionary
|
||||||
|
..strokes
|
||||||
|
|
||||||
|
) = {
|
||||||
|
let strokes = strokes.pos().filter(x => x != auto).rev()
|
||||||
|
return strokes.fold(stroke(), update-stroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(merge-fills(red, blue), red)
|
||||||
|
#assert.eq(merge-fills(red, blue, green), red)
|
||||||
|
#assert.eq(merge-fills(auto, blue, green), blue)
|
||||||
|
#assert.eq(merge-fills(auto, auto, auto, black, red), black)
|
||||||
|
#assert.eq(merge-fills(auto), auto)
|
||||||
|
#assert.eq(merge-fills(none), none)
|
||||||
|
|
||||||
|
|
||||||
|
#assert.eq(merge-strokes(auto), stroke())
|
||||||
|
#assert.eq(merge-strokes(none), none)
|
||||||
|
#assert.eq(merge-strokes(1pt), stroke(1pt))
|
||||||
|
#assert.eq(merge-strokes(1pt, black), 1pt + black)
|
||||||
|
#assert.eq(merge-strokes(auto, red), stroke(red))
|
||||||
|
#assert.eq(merge-strokes(red, auto), stroke(red))
|
||||||
|
#assert.eq(merge-strokes(red, stroke(dash: "dotted"), 4pt, 10pt), stroke(paint: red, dash: "dotted", thickness: 4pt))
|
||||||
|
#assert.eq(merge-strokes(stroke(), 1pt, tiling("1"), red, auto, stroke()), 1pt + tiling("1"))
|
||||||
|
#assert.eq(merge-strokes(none, stroke(), 1pt, tiling("1"), red, auto, stroke()), none)
|
||||||
|
#assert.eq(merge-strokes(1pt, none, tiling("1"), red, auto, stroke()), stroke(1pt))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/// Brings a margin argument into "normal" form, which is a dictionary with the
|
||||||
|
/// (all obligatory) keys `left`, `right`, `top`, and `bottom`.
|
||||||
|
/// The input can either be a single `ratio` or a dictionary
|
||||||
|
/// with the possible keys
|
||||||
|
/// - `left`, `right`, `top`, and `bottom`,
|
||||||
|
/// - `x` and `y` for combined `left`/`right` and `top`/`bottom`, respectively,
|
||||||
|
/// - and `rest` for all otherwise unspecified values.
|
||||||
|
///
|
||||||
|
/// -> dictionary
|
||||||
|
#let process-margin(
|
||||||
|
|
||||||
|
/// The margin argument to process.
|
||||||
|
/// -> ratio | dictionary
|
||||||
|
margin
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if type(margin) == ratio {
|
||||||
|
(left: margin, right: margin, top: margin, bottom: margin)
|
||||||
|
} else if type(margin) == dictionary {
|
||||||
|
let rest = margin.at("rest", default: 5%)
|
||||||
|
if not "left" in margin { margin.left = margin.at("x", default: rest) }
|
||||||
|
if not "right" in margin { margin.right = margin.at("x", default: rest) }
|
||||||
|
if not "top" in margin { margin.top = margin.at("y", default: rest) }
|
||||||
|
if not "bottom" in margin { margin.bottom = margin.at("y", default: rest) }
|
||||||
|
margin
|
||||||
|
} else {
|
||||||
|
assert(false, message: "The type `" + str(type(margin)) + "` is not valid for margin arguments. ")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/// Makes a 2D alignment out of a 2D or 1D alignment. If the input is already a 2D
|
||||||
|
/// alignment, it is returned unchanged. Else, it is complemented with the respective
|
||||||
|
/// defaults for vertical and horizontal alignment.
|
||||||
|
///
|
||||||
|
/// -> alignment
|
||||||
|
#let twod-ify-alignment(
|
||||||
|
|
||||||
|
/// Alignment to 2D-ify.
|
||||||
|
/// -> alignment
|
||||||
|
alignment,
|
||||||
|
|
||||||
|
/// Default vertical alignment to apply if the input provides just a horizontal alignment.
|
||||||
|
/// -> alignment
|
||||||
|
vertical: horizon,
|
||||||
|
|
||||||
|
/// Default horizontal alignment to apply if the input provides just a vertical alignment.
|
||||||
|
/// -> alignment
|
||||||
|
horizontal: center
|
||||||
|
|
||||||
|
) = {
|
||||||
|
if alignment.axis() == none { return alignment }
|
||||||
|
if alignment.axis() == "vertical" {
|
||||||
|
return alignment + horizontal
|
||||||
|
}
|
||||||
|
return alignment + vertical
|
||||||
|
}
|
||||||
|
|
||||||
|
#assert.eq(twod-ify-alignment(top + right), top + right)
|
||||||
|
#assert.eq(twod-ify-alignment(top + left), top + left)
|
||||||
|
#assert.eq(twod-ify-alignment(top), top + center)
|
||||||
|
#assert.eq(twod-ify-alignment(bottom), bottom + center)
|
||||||
|
#assert.eq(twod-ify-alignment(horizon), horizon + center)
|
||||||
|
#assert.eq(twod-ify-alignment(left), left + horizon)
|
||||||
|
#assert.eq(twod-ify-alignment(right), right + horizon)
|
||||||
|
#assert.eq(twod-ify-alignment(center), center + horizon)
|
||||||
|
#assert.eq(twod-ify-alignment(center + bottom), center + bottom)
|
||||||
|
#assert.eq(twod-ify-alignment(right, vertical: bottom), right + bottom)
|
||||||
|
#assert.eq(twod-ify-alignment(top, horizontal: right), top + right)
|
||||||
|
|
||||||
|
|
||||||
|
#let process-grid-arg(grid) = {
|
||||||
|
if grid == none { return (stroke: none, stroke-sub: none) }
|
||||||
|
if grid == auto { return (:) }
|
||||||
|
if type(grid) == dictionary { return grid }
|
||||||
|
if type(grid) in (color, stroke, length) {
|
||||||
|
return (stroke: grid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user