Added support for independant variable nested

equations
This commit is contained in:
Jomar 2026-04-16 16:42:53 -07:00
parent ab696338f4
commit 48ad7dfb9d
4 changed files with 303 additions and 204 deletions

View file

@ -371,7 +371,7 @@ export function evaluateExpressions(parsedText: parsedText, xRange: [number,numb
compiledNested[obj.signature] = parsed // expr is an array
}
else {
compiledNested[obj.signature] = math.compile(expr); // expr is supposed to be a valid equation. We compile it and the compiled object.
compiledNested[obj.signature] = (expr); // expr is supposed to be a valid equation.
}
}
@ -398,20 +398,20 @@ export function evaluateExpressions(parsedText: parsedText, xRange: [number,numb
if (!isConstant) {
scope[variable] = val;
}
for (let name in compiledNested) {
const v = compiledNested[name]; // grabs the expression with "name" signature
for (let signature in compiledNested) {
const nestedExpr = compiledNested[signature]; // grabs the expression with "name" signature
if (v === undefined) continue;
if (nestedExpr === undefined) continue;
if (typeof v === "number") {
scope[name] = v; // if the expression is a scalar (constant). Simply assign it to the scope with the signature as its variable.
if (typeof nestedExpr === "number") {
scope[signature] = nestedExpr; // if the expression is a scalar (constant). Simply assign it to the scope with the signature as its variable.
}
else if (Array.isArray(v)) { // if the expression is a list of values the current equation needs to be evaluated with the current val
else if (Array.isArray(nestedExpr)) { // if the expression is a list of values the current equation needs to be evaluated with the current val
// at each value in the array.
// So if f(x) = x^2 * D -> D: [0.3,0.4,0.5]. We get three plots, one for each D value.
// ONLY the equation that contains the nested function needs to be plotted. Other functions work as normal.
if ( vars.includes(name) ) { // Checks if the signature that matches the list is a variable of the current equation.
if ( vars.includes(signature) ) { // Checks if the signature that matches the list is a variable of the current equation.
// Essentially evaluateExpressions needs to be called for each one. But evaluateExpressions has too much logic not need at this point.
// I also need to stop evaluating this equation entirely.
@ -419,12 +419,32 @@ export function evaluateExpressions(parsedText: parsedText, xRange: [number,numb
// Then continue to the next equation
if (variable === undefined) continue equationLoop;
results.push(...handleNestedArray(equation,variable,localRange,v,name));
results.push(...handleNestedArray(equation.expr,variable,localRange,nestedExpr,signature,scope));
continue equationLoop;
}
}
else {
scope[name] = v.evaluate(scope); // if the expression is neither a scalar nor an array then its an expression. evaluate it with normal the current val
const xranges = parsedText.lineProperties.map(prop => `${prop.signature}.${prop.property}=${prop.value}`).filter(s => s.includes("xrange"));
const indexRange = xranges.findIndex(s => s.startsWith(`${signature}.xrange=`));
if (indexRange !== -1){
// As of version 1.0.3 I want to support independant variables inside the nested equation.
// Re = DV/v
// D: 0.3
// v: 2T + 10
// v.xrange = [100,300,50] Which creates a reynolds plot for each viscosity. Where Velocity is the main variable
if (!xranges[indexRange]) continue equationLoop;
const nestedLocalRangeString = xranges[indexRange].split("=")[1];
if (!nestedLocalRangeString) continue equationLoop;
let nestedLocalRange: [number,number,number] = JSON.parse(nestedLocalRangeString);
if (variable === undefined) continue equationLoop;
results.push(...handleNestedIndependant(equation,variable,localRange,nestedLocalRange,nestedExpr,signature, nestInfo,scope));
continue equationLoop;
} else {
const compiledNestedExpr = math.compile(nestedExpr);
scope[signature] = compiledNestedExpr.evaluate(scope); // if the expression is neither a scalar nor an array then its an expression. evaluate it with normal the current val
}
}
}
@ -467,18 +487,20 @@ export function getVariable(expr: Equation | NestedEquations | RawExpr) {
}
function isNumberString(val: string): boolean {
return val.trim() !== "" && !isNaN(Number(val));
}
function handleNestedArray(eq: Equation,variable: string, localRange: [number, number, number], v: number[], name: string) {
const expr = math.compile(eq.expr);
function handleNestedArray(mainExpr: string ,variable: string, localRange: [number, number, number], v: number[], name: string, baseScope: Record<string, string | number>, legendName?: string): PlotData[] {
const expr = math.compile(mainExpr);
const results: PlotData[] = [];
for (let i of v) {
const mDataPoints: Data[] = []
for (let val = localRange[0]; val <= localRange[1]; val += localRange[2]) {
const scope = {
...baseScope,
[variable]: val,
[name]: i
}
@ -491,11 +513,46 @@ function handleNestedArray(eq: Equation,variable: string, localRange: [number, n
}
mDataPoints.push({x:val,y});
}
results.push({signature: `${name}=${i}`, data: mDataPoints});
if (!legendName) results.push({signature: `${name}=${i}`, data: mDataPoints});
else results.push({signature: `${legendName}=${i}`, data: mDataPoints});
}
return results;
}
function handleNestedIndependant(eq: Equation, variable: string, localRange: [number,number,number],
nestedLocalRange: [number,number,number], nestedExpr: any, signature: string, nestInfo: string[][], baseScope: Record<string, string | number>): PlotData[]{
const nestedResults: number[] = [];
// Get Main Equation Variable --------------------------------------
const nestedEquationObject: Equation = {
expr: nestedExpr,
signature:signature
};
const vars = getVariable(nestedEquationObject).filter((v) => !builtInConstants.includes(v));
// vars can include builtInConstants that need to be filtered out so they arent recognized as the variable.
const newVars = vars.flatMap(v => {
if (nestInfo[1]?.includes(v)) {
return [];
}
return [v];
})
// vars also filters out any variabales that are actually nested function signatures. Think if f(x) = G + x -> filters out G as long as G is declared as G: val
const nestedVariable = newVars[0];
if (!nestedVariable) return [];
const nestedEquation = math.compile(nestedExpr);
for (let i = nestedLocalRange[0]; i <= nestedLocalRange[1]; i += nestedLocalRange[2]){
const nestedScope: Record<string, number | string> = {
...baseScope,
};
nestedScope[nestedVariable] = i;
let nestedY = nestedEquation.evaluate(nestedScope);
nestedResults.push(nestedY);
};
return handleNestedArray(eq.expr, variable, localRange, nestedResults, signature, baseScope, nestedVariable);
}
function handleDiscontinuities(mDataPoints: Data[], localRange: [number, number, number], y: number) {
// ---------------- Discontinuities ---------------------------------------
const prev = mDataPoints[mDataPoints.length - 1];

194
helpers/svgFormatting.ts Normal file
View file

@ -0,0 +1,194 @@
import { ChartType} from "chart.js/auto";
import {Data} from "./parser";
export function generateSVG(chartInstance: any, chartType: ChartType) {
const width = chartInstance.width;
const height = chartInstance.height;
//const backgroundColor = this.settings.backgroundColor;
const xScale = chartInstance.scales.x;
const yScale = chartInstance.scales.y;
const area = chartInstance.chartArea;
const style = getComputedStyle(document.body);
const bg = style.getPropertyValue("--background-secondary").trim() || "#ffffff";
const text = style.getPropertyValue("--text-normal").trim() || "#000000";
const muted = style.getPropertyValue("--text-muted").trim() || "#666666";
const border = style.getPropertyValue("--background-modifier-border").trim() || "#d0d0d0";
let svg = "";
switch(chartType) {
case "line": {
svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="${bg}" />`;
svg += `
<line
x1="${area.left}"
y1="${area.bottom}"
x2="${area.right}"
y2="${area.bottom}"
stroke="${muted}"
stroke-width="1"
/>
<line
x1="${area.left}"
y1="${area.top}"
x2="${area.left}"
y2="${area.bottom}"
stroke="${muted}"
stroke-width="1"
/>
`;
const title = chartInstance.options?.plugins?.title;
if (title?.display && title.text) {
svg += `
<text
x="${width / 2}"
y="20"
font-size="16"
font-weight="bold"
text-anchor="middle"
fill="${text}">${title.text}</text>
`;
}
xScale.ticks.forEach((tick: any, i: number) => {
const x = xScale.getPixelForTick(i);
svg += `
<line
x1="${x}"
y1="${area.top}"
x2="${x}"
y2="${area.bottom}"
stroke="${border}"
stroke-width="1"
/>
`;
svg += `
<line
x1="${x}"
y1="${area.bottom}"
x2="${x}"
y2="${area.bottom + 6}"
stroke="${border}"
stroke-width="1"
/>
<text
x="${x}"
y="${area.bottom + 18}"
font-size="12"
text-anchor="middle"
dominant-baseline="middle"
fill="${text}">
${tick.label}
</text>
`;
});
yScale.ticks.forEach((tick: any, i: number) => {
const y = yScale.getPixelForTick(i);
svg += `
<line
x1="${area.left}"
y1="${y}"
x2="${area.right}"
y2="${y}"
stroke="${border}"
stroke-width="1"
/>
`;
svg += `
<line
x1="${area.left - 6}"
y1="${y}"
x2="${area.left}"
y2="${y}"
stroke="${border}"
stroke-width="1"
/>
<text
x="${area.left - 8}"
y="${y}"
font-size="12"
text-anchor="end"
dominant-baseline="middle"
fill="${text}">${tick.label}</text>
`;
});
let lx = width - 74;
let ly = 25;
chartInstance.data.datasets.forEach((dataset: any, i: number) => {
const y = ly + i*20;
const meta = chartInstance.getDatasetMeta(i);
const points = meta.data.map((pt: Data) => `${pt.x},${pt.y}`).join(" ");
const color = dataset.borderColor || "black";
const radius = (dataset.pointRadius && dataset.pointRadius === 0) ? 0 : dataset.pointRadius;
//const radius = dataset.pointRadius && dataset.pointRadius > 0 ? dataset.pointRadius : 3;
svg += `
<rect x="${lx}" y="${y-10}" width="12" height="12" fill="${color}" />
<text
x="${lx + 18}"
y="${y}"
font-size="12"
dominant-baseline="middle"
fill="${text}">${dataset.label || `Series ${i+1}`}</text>
<polyline
fill="none"
stroke="${color}"
stroke-width="${dataset.borderWidth || 2}"
points="${points}"
/>`;
meta.data.forEach((pt: Data) => {
svg += `
<circle
cx="${pt.x}"
cy="${pt.y}"
r="${radius}"
fill="${color}"
/>
`;
});
});
const xTitle = chartInstance.options?.scales?.x?.title;
if (xTitle?.display && xTitle.text) {
svg += `
<text
x="${(area.left + area.right) / 2}"
y="${height - 8}"
font-size="13"
font-weight="bold"
text-anchor="middle"
fill="${text}">${xTitle.text}</text>
`;
}
const yTitle = chartInstance.options?.scales?.y?.title;
if (yTitle?.display && yTitle.text) {
svg += `
<text
x="16"
y="${(area.top + area.bottom) / 2}"
font-size="13"
font-weight="bold"
text-anchor="middle"
fill="${text}"
transform="rotate(-90 16 ${(area.top + area.bottom) / 2})">
${yTitle.text}
</text>
`;
}
svg += `</svg>`;
break;
}
}
return svg
}

View file

@ -1,18 +1,19 @@
import {handleMarkdown, handleGlobalOptions, evaluateExpressions, PlotData, parsedText, handleTableData, Equation, GlobalProperties,Data} from "../helpers/parser"
import {handleMarkdown, evaluateExpressions, PlotData, parsedText, handleTableData, Equation, GlobalProperties,Data} from "../helpers/parser"
import { createPlot, buildDatasets} from "../helpers/graphs";
import {Menu, Notice, Plugin, MarkdownView} from "obsidian";
import { apply } from "mathjs";
import { setApp } from "../helpers/appContext";
import { PlotPluginSettings, DEFAULT_SETTINGS, PlotSettingTab } from "settings";
import { ChartOptions, ChartType, Ticks } from "chart.js/auto";
import { ChartOptions, ChartType} from "chart.js/auto";
import {autocompletion} from "@codemirror/autocomplete";
import {generateSVG} from "../helpers/svgFormatting";
import { PlotCompletionSource } from "../helpers/plotProperties";
import { zoom } from "chartjs-plugin-zoom";
import { divideScalarDependencies } from "mathjs";
import { codePointSize } from "@codemirror/state";
import { e } from "mathjs";
import { simplify } from "mathjs";
export default class PlotPlugin extends Plugin {
settings!: PlotPluginSettings;
@ -40,6 +41,7 @@ export default class PlotPlugin extends Plugin {
const defaultProperties: ChartOptions<ChartType> = getDefaultPlotProperties(this.settings,chartType);
const sourcePaths = getSourcePaths(newMarkdown); // Gets any source:: item from path PATHS. Stores them for comparisons later.
if (defaultProperties === undefined) return;
let cachedParsedText = await handleMarkdown(newMarkdown,defaultProperties,chartType); // Evaluates all the markdown in the codeblock and creates a ParsedText type object.
let chartInstance: any = undefined;
@ -228,7 +230,7 @@ export default class PlotPlugin extends Plugin {
.setTitle("Save SVG to Vault")
.setIcon("image-file")
.onClick( async () => {
await this.exportChartSVG(chartInstance)
await this.exportChartSVG(chartInstance,chartType)
}));
menu.showAtPosition({x: e.pageX, y: e.pageY});
@ -251,193 +253,10 @@ export default class PlotPlugin extends Plugin {
}
await this.app.vault.createBinary(path,bytes.buffer);
}
async exportChartSVG(chartInstance: any, path = `${this.settings.saveImagesPath}/Chart_${Date.now()}.svg`) {
const width = chartInstance.width;
const height = chartInstance.height;
//const backgroundColor = this.settings.backgroundColor;
const xScale = chartInstance.scales.x;
const yScale = chartInstance.scales.y;
const area = chartInstance.chartArea;
const style = getComputedStyle(document.body);
const bg = style.getPropertyValue("--background-secondary").trim() || "#ffffff";
const text = style.getPropertyValue("--text-normal").trim() || "#000000";
const muted = style.getPropertyValue("--text-muted").trim() || "#666666";
const border = style.getPropertyValue("--background-modifier-border").trim() || "#d0d0d0";
let svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}"><rect width="100%" height="100%" fill="${bg}" />`;
svg += `
<line
x1="${area.left}"
y1="${area.bottom}"
x2="${area.right}"
y2="${area.bottom}"
stroke="${muted}"
stroke-width="1"
/>
<line
x1="${area.left}"
y1="${area.top}"
x2="${area.left}"
y2="${area.bottom}"
stroke="${muted}"
stroke-width="1"
/>
`;
const title = chartInstance.options?.plugins?.title;
if (title?.display && title.text) {
svg += `
<text
x="${width / 2}"
y="20"
font-size="16"
font-weight="bold"
text-anchor="middle"
fill="${text}">${title.text}</text>
`;
}
xScale.ticks.forEach((tick: any, i: number) => {
const x = xScale.getPixelForTick(i);
svg += `
<line
x1="${x}"
y1="${area.top}"
x2="${x}"
y2="${area.bottom}"
stroke="${border}"
stroke-width="1"
/>
`;
svg += `
<line
x1="${x}"
y1="${area.bottom}"
x2="${x}"
y2="${area.bottom + 6}"
stroke="${border}"
stroke-width="1"
/>
<text
x="${x}"
y="${area.bottom + 18}"
font-size="12"
text-anchor="middle"
dominant-baseline="middle"
fill="${text}">
${tick.label}
</text>
`;
});
yScale.ticks.forEach((tick: any, i: number) => {
const y = yScale.getPixelForTick(i);
svg += `
<line
x1="${area.left}"
y1="${y}"
x2="${area.right}"
y2="${y}"
stroke="${border}"
stroke-width="1"
/>
`;
svg += `
<line
x1="${area.left - 6}"
y1="${y}"
x2="${area.left}"
y2="${y}"
stroke="${border}"
stroke-width="1"
/>
<text
x="${area.left - 8}"
y="${y}"
font-size="12"
text-anchor="end"
dominant-baseline="middle"
fill="${text}">${tick.label}</text>
`;
});
let lx = width - 74;
let ly = 25;
chartInstance.data.datasets.forEach((dataset: any, i: number) => {
const y = ly + i*20;
const meta = chartInstance.getDatasetMeta(i);
const points = meta.data.map((pt: Data) => `${pt.x},${pt.y}`).join(" ");
const color = dataset.borderColor || "black";
const radius = (dataset.pointRadius && dataset.pointRadius === 0) ? 0 : dataset.pointRadius;
//const radius = dataset.pointRadius && dataset.pointRadius > 0 ? dataset.pointRadius : 3;
svg += `
<rect x="${lx}" y="${y-10}" width="12" height="12" fill="${color}" />
<text
x="${lx + 18}"
y="${y}"
font-size="12"
dominant-baseline="middle"
fill="${text}">${dataset.label || `Series ${i+1}`}</text>
<polyline
fill="none"
stroke="${color}"
stroke-width="${dataset.borderWidth || 2}"
points="${points}"
/>`;
meta.data.forEach((pt: Data) => {
svg += `
<circle
cx="${pt.x}"
cy="${pt.y}"
r="${radius}"
fill="${color}"
/>
`;
});
});
const xTitle = chartInstance.options?.scales?.x?.title;
if (xTitle?.display && xTitle.text) {
svg += `
<text
x="${(area.left + area.right) / 2}"
y="${height - 8}"
font-size="13"
font-weight="bold"
text-anchor="middle"
fill="${text}">${xTitle.text}</text>
`;
}
const yTitle = chartInstance.options?.scales?.y?.title;
if (yTitle?.display && yTitle.text) {
svg += `
<text
x="16"
y="${(area.top + area.bottom) / 2}"
font-size="13"
font-weight="bold"
text-anchor="middle"
fill="${text}"
transform="rotate(-90 16 ${(area.top + area.bottom) / 2})">
${yTitle.text}
</text>
`;
}
svg += `</svg>`;
async exportChartSVG(chartInstance: any, chartType: ChartType, path = `${this.settings.saveImagesPath}/Chart_${Date.now()}.svg`) {
const svg = generateSVG(chartInstance,chartType);
await this.app.vault.create(path, svg);
}
}
}
@ -494,7 +313,7 @@ export function customNotice(msg: string, cls = "", timeout = 4000) {
const notice = new Notice(msg, timeout);
requestAnimationFrame(() => {
const el = notice.noticeEl;
const el = notice.containerEl;
if (el && cls) el.classList.add(cls);
});
}
@ -506,7 +325,27 @@ export function getDefaultPlotProperties(settings: PlotPluginSettings, chartType
};
}
function showError(container: HTMLElement, title: string, err: unknown) {
const msg = err instanceof Error ? err.message: String(err);
container.empty();
const box = container.createDiv("datachart-error");
box.createEl("div",{text: title, cls: "datachart-error-title"});
box.createEl("div",{text: simplifyError(msg)});
console.error(err);
}
function simplifyError(err: unknown): string {
const msg = err instanceof Error ? err.message : String(err);
if (msg.includes("Undefined symbol")) {
const m = msg.match(/Undefined symbol (\w+)/);
if (m) return `Unknown variable: ${m[1]}. Variables written as var(x) are not currently supported.`;
}
if (msg.includes("Unexpected token")) return "Syntax error in expression";
if (msg.includes("Cannot read")) return "Missing property or invalid object path.";
return msg;
}
export function getTypeDefaults(chartType: ChartType, settings: PlotPluginSettings): ChartOptions<ChartType> {
const style = getComputedStyle(document.body);

View file

@ -56,3 +56,12 @@
box-shadow:none ;
}
.datachart-error{
padding:12px;
border-radius:8px;
background: var(--background-secondary);
border:1px solid var(--color-red);
color: var(--text-normal);
font-family: var(--font-monospace);
white-space: pre-wrap;
}