├── .prettierrc.json ├── .yarnrc.yml ├── images ├── plot │ ├── dark.png │ └── light.png ├── settings │ ├── dark.png │ └── light.png └── create-modal │ ├── dark.png │ └── light.png ├── .devcontainer └── devcontainer.json ├── .eslintignore ├── .prettierignore ├── src ├── __mocks__ │ └── obsidian.ts ├── styles.scss ├── __tests__ │ ├── integrity.test.ts │ └── utils.test.ts ├── common │ ├── defaults.ts │ ├── types.ts │ └── utils.ts ├── plugins │ └── styling.ts ├── main.ts └── app │ ├── CreatePlotModal.ts │ └── SettingsTab.ts ├── versions.json ├── tsconfig.json ├── .github ├── labeler.yml ├── workflows │ ├── labeler.yml │ ├── delete-runs.yml │ ├── release.yml │ └── ci.yml ├── dependabot.yml ├── version-bump.mjs └── ISSUE_TEMPLATE │ ├── FEATURE_REQUEST.yml │ └── BUG_REPORT.yml ├── jest.config.ts ├── manifest.json ├── .editorconfig ├── .eslintrc.json ├── webpack.config.cjs ├── CONTRIBUTING.md ├── LICENSE ├── package.json ├── README.md └── .gitignore /.prettierrc.json: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | -------------------------------------------------------------------------------- /images/plot/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/plot/dark.png -------------------------------------------------------------------------------- /images/plot/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/plot/light.png -------------------------------------------------------------------------------- /images/settings/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/settings/dark.png -------------------------------------------------------------------------------- /images/settings/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/settings/light.png -------------------------------------------------------------------------------- /images/create-modal/dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/create-modal/dark.png -------------------------------------------------------------------------------- /images/create-modal/light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/leonhma/obsidian-functionplot/HEAD/images/create-modal/light.png -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-functionplot", 3 | "onCreateCommand": "yarn install --non-interactive" 4 | } 5 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | main.js 4 | data.json 5 | package-lock.json 6 | .nyc_output 7 | coverage 8 | 9 | LICENSE 10 | .deepsource.toml 11 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | main.js 4 | data.json 5 | package-lock.json 6 | .nyc_output 7 | coverage 8 | 9 | LICENSE 10 | .deepsource.toml 11 | -------------------------------------------------------------------------------- /src/__mocks__/obsidian.ts: -------------------------------------------------------------------------------- 1 | import { parse } from "yaml"; 2 | 3 | /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ 4 | export function parseYaml(text: string): any { 5 | return parse(text); 6 | } 7 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "1.0.0": "0.11.13", 3 | "1.0.1": "0.11.13", 4 | "1.0.2": "0.11.13", 5 | "1.0.3": "0.11.13", 6 | "1.1.0": "0.12.2", 7 | "1.1.1": "0.12.2", 8 | "1.2.0": "0.12.2", 9 | "1.2.1": "0.12.2" 10 | } 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": ["./src/main.ts"], 3 | "compilerOptions": { 4 | "moduleResolution": "node", 5 | "module": "ES6", 6 | "esModuleInterop": true, 7 | "target": "ES6", 8 | "allowJs": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | docs: 2 | - "README.md" 3 | - "images/**/*" 4 | 5 | code: 6 | - "src/**/*.ts" 7 | 8 | style: 9 | - "src/styles.scss" 10 | 11 | automation: 12 | - ".github/workflows/*" 13 | 14 | invalid: 15 | - "LICENSE" 16 | - "versions.json" 17 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: Labeler 2 | 3 | on: pull_request_target 4 | 5 | jobs: 6 | label: 7 | runs-on: ubuntu-latest 8 | 9 | steps: 10 | - name: Labeler 11 | uses: actions/labeler@v4.0.0 12 | with: 13 | repo-token: ${{ github.token }} 14 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | console.log('jest config loaded') 2 | 3 | export default { 4 | roots: ["/src"], 5 | testMatch: ["**/__tests__/**/*.+(ts)"], 6 | preset: "ts-jest/presets/js-with-ts", 7 | testEnvironment: "node", 8 | transformIgnorePatterns: ["/node_modules/"], 9 | }; 10 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: / 5 | schedule: 6 | interval: daily 7 | versioning-strategy: increase-if-necessary 8 | ignore: 9 | - dependency-name: obsidian 10 | - dependency-name: "*" 11 | update-types: ["version-update:semver-patch"] 12 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "obsidian-functionplot", 3 | "name": "Obsidian Functionplot", 4 | "minAppVersion": "0.16.3", 5 | "description": "A plugin for displaying mathematical graphs in obsidian.md.", 6 | "author": "leonhma", 7 | "authorUrl": "https://github.com/leonhma", 8 | "isDesktopOnly": false, 9 | "version": "1.2.1" 10 | } 11 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | .obsfplt-rendertarget { 2 | color: var(--text-normal); 3 | width: 100%; 4 | height: 100%; 5 | border-radius: 0.5em; 6 | } 7 | 8 | .obsfplt-error { 9 | border-radius: 1em; 10 | background: black; 11 | opacity: 0.5; 12 | 13 | p { 14 | opacity: 1; 15 | font-size: 0.7em; 16 | padding: 1em; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | # Unix-style newlines with a newline ending every file 7 | [*] 8 | end_of_line = lf 9 | insert_final_newline = true 10 | indent_style = space 11 | indent_size = 2 12 | 13 | [**.md] 14 | trim_trailing_whitespace = false 15 | indent_size = 4 16 | -------------------------------------------------------------------------------- /src/__tests__/integrity.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from "fs"; 2 | 3 | test("minAppVersion and obsidian version in package.json match", () => { 4 | // read files 5 | const package_ = JSON.parse(readFileSync("package.json", "utf8")); 6 | const manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 7 | expect(manifest.minAppVersion).toBe(package_.dependencies.obsidian); 8 | }); 9 | -------------------------------------------------------------------------------- /src/__tests__/utils.test.ts: -------------------------------------------------------------------------------- 1 | import { parseCodeBlock } from "../common/utils"; 2 | 3 | describe("parseCodeBlock", () => { 4 | test("header is parsed correctly", () => { 5 | const result = parseCodeBlock("---\ntitle: test\n---\n\n"); 6 | expect(result).toStrictEqual([{ title: "test" }, []]); 7 | }); 8 | test("functions are parsed correctly", () => { 9 | const result = parseCodeBlock("f(x) = x"); 10 | expect(result).toStrictEqual([{}, ["f(x) = x"]]); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "eslint:recommended", 4 | "plugin:@typescript-eslint/recommended", 5 | "prettier" 6 | ], 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "es6": true 11 | }, 12 | "parser": "@typescript-eslint/parser", 13 | "plugins": ["@typescript-eslint"], 14 | 15 | "root": true, 16 | 17 | "rules": { 18 | "no-unused-vars": "error", 19 | "@typescript-eslint/no-unused-vars": "off", 20 | "no-use-before-define": "warn" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/version-bump.mjs: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | 3 | const version = process.argv[2]; 4 | 5 | // read files 6 | let package_ = JSON.parse(readFileSync("package.json", "utf8")); 7 | let manifest = JSON.parse(readFileSync("manifest.json", "utf8")); 8 | let versions = JSON.parse(readFileSync("versions.json", "utf8")); 9 | 10 | const minAppVersion = manifest.minAppVersion; 11 | 12 | // index minAppVersion 13 | versions[version] = minAppVersion; 14 | 15 | // update versions 16 | package_.version = version; 17 | manifest.version = version; 18 | 19 | writeFileSync("package.json", JSON.stringify(package_, null, "\t")); 20 | writeFileSync("manifest.json", JSON.stringify(manifest, null, "\t")); 21 | writeFileSync("versions.json", JSON.stringify(versions, null, "\t")); 22 | -------------------------------------------------------------------------------- /.github/workflows/delete-runs.yml: -------------------------------------------------------------------------------- 1 | name: clean workflow runs 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * 0" 6 | workflow_dispatch: 7 | inputs: 8 | retention_days: 9 | required: true 10 | default: "30" 11 | description: Retention days 12 | min_keep: 13 | required: true 14 | default: "6" 15 | description: Minimum runs to keep 16 | 17 | jobs: 18 | del_runs: 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Delete workflow runs 22 | uses: Mattraks/delete-workflow-runs@v2 23 | with: 24 | token: ${{ github.token }} 25 | repository: ${{ github.repository }} 26 | retain_days: ${{ github.event.inputs.retention_days || 30 }} 27 | keep_minimum_runs: ${{ github.event.inputs.min_keep || 6 }} 28 | -------------------------------------------------------------------------------- /webpack.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | mode: "production", 3 | entry: "./src/main.ts", 4 | module: { 5 | rules: [ 6 | { 7 | test: /\.ts$/, 8 | use: "ts-loader", 9 | exclude: /node_modules/, 10 | }, 11 | { 12 | test: /\.s[ac]ss$/i, 13 | use: [ 14 | // Creates `style` nodes from JS strings 15 | "style-loader", 16 | // Translates CSS into CommonJS 17 | "css-loader", 18 | // Compiles Sass to CSS 19 | "sass-loader", 20 | ], 21 | }, 22 | ], 23 | }, 24 | resolve: { 25 | extensions: [".ts", ".js"], 26 | }, 27 | externals: { 28 | obsidian: "commonjs obsidian", 29 | }, 30 | output: { 31 | path: __dirname, 32 | filename: "main.js", 33 | libraryTarget: "commonjs", 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contibutions are always welcome! We appreciate every help with this project, be it just submitting issues or feature requests, picking one up and working on it, making edits to the wiki or opening a pull request with a new feature or a bugfix, as it helps grow the project. 4 | 5 | TL;DR: 6 | 7 | - The `minAppVersion` should be kept up-to-date 8 | 9 | ## Rules 10 | 11 | Because obsidian keeps an index of versions of the plugin and their respective minimum version requirements for obsidian itself, the `minAppVersion` field in `manifest.json` and the version of the dependency `obsidian` in `package.json` should be kept equal. The version should also reflect the lowest version of api under which the plugin will work. The dependency version in `package.json` is pinned to a single version. 12 | 13 | Oh, and also: This project uses yarn, although it doesn't matter if you use another package manager so long as you don't commit the log and lock files to VCS. Just be sure to let us know of your choice of package manager if you encounter a problem. 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml: -------------------------------------------------------------------------------- 1 | name: Feature request 2 | description: Request a new feature 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to make this plugin better! (Hint: If you are planning on working on this new feature yourself, you can remove the default assignees in the top-right corner and self-assign this issue.) 9 | - type: textarea 10 | id: request 11 | attributes: 12 | label: What's your feature? 13 | description: Describe the feature you want! 14 | placeholder: Description of your solution.. 15 | validations: 16 | required: true 17 | - type: textarea 18 | id: related 19 | attributes: 20 | label: Any related problems? 21 | description: Is your feature request related to a problem? Please describe. 22 | placeholder: Related problems.. 23 | - type: textarea 24 | id: additional 25 | attributes: 26 | label: Additional context? 27 | description: If you want to add screenshots, code, etc., please do so here. 28 | placeholder: Additional info.. 29 | -------------------------------------------------------------------------------- /src/common/defaults.ts: -------------------------------------------------------------------------------- 1 | import { rendererType, PlotOptions, PluginSettings } from "./types"; 2 | 3 | /** 4 | * The options displayed for renderers 5 | */ 6 | // eslint-disable-next-line no-unused-vars 7 | export const rendererOptions: { [_ in rendererType]: string } = { 8 | interactive: "Interactive (zoomable)", 9 | //image: "Image (exportable)", 10 | }; 11 | 12 | /** 13 | * The default options for a plot. 14 | */ 15 | export const DEFAULT_PLOT_OPTIONS: PlotOptions = { 16 | title: "", 17 | xLabel: "", 18 | yLabel: "", 19 | bounds: [-10, 10, -10, 10], 20 | disableZoom: false, 21 | grid: true, 22 | functions: [], 23 | }; 24 | 25 | /** 26 | * The default plugin settings. 27 | */ 28 | export const DEFAULT_PLUGIN_SETTINGS: PluginSettings = { 29 | titleFontSize: 24, 30 | scaleFontSize: 12, 31 | labelFontSize: 12, 32 | // annotationFontSize: 16, 33 | 34 | lineWidth: 2, 35 | gridWidth: 1, 36 | 37 | fontColor: "var(--text-normal)", 38 | // annotationColor: '#000', 39 | lineColor: "gray", 40 | gridColor: "var(--interactive-hover)", 41 | 42 | defaultRenderer: "interactive", 43 | 44 | telemetry: true, 45 | }; 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Leonhard Masche 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-functionplot", 3 | "version": "1.2.1", 4 | "description": "A plugin for displaying mathematical functions in obsidian.md.", 5 | "repository": "https://github.com/leonhma/obsidian-functionplot", 6 | "type": "module", 7 | "author": "leonhma", 8 | "license": "MIT", 9 | "private": true, 10 | "scripts": { 11 | "build": "webpack", 12 | "test": "jest", 13 | "format": "yarn prettier --write .", 14 | "lint": "yarn eslint ." 15 | }, 16 | "devDependencies": { 17 | "@types/jest": "^29.2.0", 18 | "@types/node": "*", 19 | "@typescript-eslint/eslint-plugin": "^5.41.0", 20 | "@typescript-eslint/parser": "^5.41.0", 21 | "eslint": "^8.26.0", 22 | "eslint-config-prettier": "^8.5.0", 23 | "jest": "^29.2.2", 24 | "prettier": "^2.7.1", 25 | "sass": "^1.54.4", 26 | "sass-loader": "^13.0.2", 27 | "ts-jest": "^29.0.3", 28 | "ts-loader": "^9.3.1", 29 | "ts-node": "^10.9.1", 30 | "typescript": "^4.7.4", 31 | "webpack": "^5.74.0", 32 | "webpack-cli": "^5.0.0", 33 | "yaml": "^2.1.3" 34 | }, 35 | "dependencies": { 36 | "function-plot": "^1.23.1", 37 | "obsidian": "0.16.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: release 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | permissions: 8 | contents: write 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Setup Node.js environment 16 | uses: actions/setup-node@v3.1.1 17 | with: 18 | node-version: 18 19 | 20 | - name: Checkout 21 | uses: actions/checkout@v2.4.2 22 | with: 23 | ref: master 24 | 25 | - name: Install packages 26 | run: yarn install 27 | 28 | - name: Run version-bump 29 | run: node .github/version-bump.mjs ${{ github.event.release.tag_name }} 30 | 31 | - name: Commit and push changes 32 | run: | 33 | git config --global user.name "leonhma" 34 | git config --global user.email "leonhardmasche@gmail.com" 35 | 36 | git add -A 37 | git commit -m "chore(versions): index minAppVersion for latest release" 38 | git push origin master 39 | 40 | - name: Build 41 | run: yarn build 42 | 43 | - name: Upload assets to a Release 44 | uses: AButler/upload-release-assets@v2.0 45 | with: 46 | repo-token: ${{ github.token }} 47 | files: "main.js;manifest.json" 48 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/BUG_REPORT.yml: -------------------------------------------------------------------------------- 1 | name: Bug report 2 | description: Create a report to help us improve 3 | labels: [bug] 4 | assignees: 5 | - leonhma 6 | body: 7 | - type: markdown 8 | attributes: 9 | value: | 10 | Thanks for taking the time to report a bug! 11 | - type: textarea 12 | id: what-happened 13 | attributes: 14 | label: What happened? 15 | description: Tell us briefly what bug happened. 16 | placeholder: What happened.. 17 | validations: 18 | required: true 19 | - type: checkboxes 20 | id: checkboxes 21 | attributes: 22 | label: Please confirm 23 | description: "Please confirm the following:" 24 | options: 25 | - label: I have checked that I am using the latest version of the plugin released. 26 | required: true 27 | - label: I have searched for similar issues, but found none. 28 | required: true 29 | - type: textarea 30 | id: ideas 31 | attributes: 32 | label: Any ideas? 33 | description: If you have an idea on how we could go about fixing this, put them here. 34 | placeholder: Your ideas.. 35 | - type: textarea 36 | id: additional 37 | attributes: 38 | label: Additional info 39 | description: Please add any additional info, such as screenshots, logs or code here. For code, use ``` backticks to get it highlighted. 40 | placeholder: Additional info.. 41 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { Chart } from "function-plot"; 2 | import { EventEmitter } from "events"; 3 | 4 | /** 5 | * The possible types of renderer. 6 | */ 7 | export type rendererType = "interactive"; //| "image"; 8 | 9 | /** 10 | * A sum-type of Chart and EventEmitter because TypeScript can't figure this out on it's own. 11 | */ 12 | export type chartType = Chart & EventEmitter; 13 | 14 | /** 15 | * An interface specifying the properties of a line. 16 | */ 17 | export interface Line { 18 | fn?: string; 19 | graphType?: "polyline" | "interval" | "scatter"; 20 | nSamples?: number; 21 | range?: [number, number]; 22 | closed?: boolean 23 | color?: string 24 | } 25 | 26 | /** 27 | * An interface specifying the options for a plot. 28 | */ 29 | export interface PlotOptions { 30 | title: string; 31 | xLabel: string; 32 | yLabel: string; 33 | bounds: [number, number, number, number]; 34 | disableZoom: boolean; 35 | grid: boolean; 36 | functions: string[]; 37 | } 38 | 39 | /** 40 | * The plugin's settings. 41 | */ 42 | export interface PluginSettings { 43 | titleFontSize: number; 44 | scaleFontSize: number; 45 | labelFontSize: number; 46 | // annotationFontSize: number 47 | lineWidth: number; 48 | gridWidth: number; 49 | 50 | fontColor: string; 51 | // annotationColor: string 52 | lineColor: string; 53 | gridColor: string; 54 | 55 | defaultRenderer: rendererType; 56 | 57 | telemetry: boolean; 58 | } 59 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - "**" 7 | 8 | jobs: 9 | build: 10 | name: Build ⚙️ 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | 15 | - uses: actions/setup-node@v3 16 | with: 17 | node-version: 18 18 | 19 | - name: install dependencies 20 | run: yarn install 21 | 22 | - name: set development version 23 | run: node .github/version-bump.mjs "dev-$(git rev-parse --short HEAD)" 24 | 25 | - name: build 26 | run: yarn build 27 | 28 | - name: upload dist 29 | uses: actions/upload-artifact@v3.1.0 30 | with: 31 | name: dist 32 | path: | 33 | manifest.json 34 | main.js 35 | 36 | test: 37 | runs-on: ubuntu-latest 38 | name: Test 🧪 39 | steps: 40 | - uses: actions/checkout@v2 41 | 42 | - uses: actions/setup-node@v3 43 | with: 44 | node-version: 18 45 | 46 | - name: install dependencies 47 | run: yarn install 48 | 49 | - name: test 50 | run: yarn test 51 | 52 | lint: 53 | runs-on: ubuntu-latest 54 | name: Lint ✔ 55 | steps: 56 | - uses: actions/checkout@v2 57 | 58 | - uses: actions/setup-node@v3 59 | with: 60 | node-version: 18 61 | 62 | - name: install dependencies 63 | run: yarn install 64 | 65 | - name: lint 66 | run: yarn lint 67 | -------------------------------------------------------------------------------- /src/plugins/styling.ts: -------------------------------------------------------------------------------- 1 | import type { chartType } from "../common/types"; 2 | import type ObsidianFunctionPlot from "../main"; 3 | 4 | export default function createStylingPlugin(plugin: ObsidianFunctionPlot) { 5 | return function stylingPlugin(instance: chartType) { 6 | if (!instance.listenerCount("after:draw")) { 7 | instance.on("after:draw", () => { 8 | const selection = instance.root.merge(instance.root.enter); 9 | 10 | selection 11 | .select(".title") 12 | .style("font-size", `${plugin.settings.titleFontSize}px`) 13 | .style("fill", plugin.settings.fontColor); 14 | 15 | selection 16 | .selectAll(".axis-label") 17 | .style("font-size", `${plugin.settings.labelFontSize}px`) 18 | .style("fill", plugin.settings.fontColor); 19 | 20 | selection 21 | .selectAll(".origin") 22 | .style("stroke", plugin.settings.lineColor) 23 | .style("stroke-width", `${plugin.settings.lineWidth}px`) 24 | .style("opacity", 1); 25 | 26 | selection 27 | .selectAll(".tick line") 28 | .style("stroke", plugin.settings.gridColor) 29 | .style("stroke-width", `${plugin.settings.gridWidth}px`) 30 | .style("opacity", 1); 31 | 32 | selection 33 | .selectAll("line.text") 34 | .style("fill", plugin.settings.fontColor) 35 | .style("font-size", `${plugin.settings.scaleFontSize}px`); 36 | 37 | selection 38 | .selectAll(".domain") 39 | .style("stroke", plugin.settings.gridColor) 40 | .style("stroke-width", `${plugin.settings.gridWidth}px`) 41 | .style("opacity", 1); 42 | 43 | instance.root.merge(selection); 44 | }); 45 | instance.emit("after:draw"); // self-emit event, remove after mauriciopoppe/function-plot#208 46 | } 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { MarkdownPostProcessorContext, Plugin, Editor } from "obsidian"; 2 | import CreatePlotModal from "./app/CreatePlotModal"; 3 | import SettingsTab from "./app/SettingsTab"; 4 | import { createPlot, parseCodeBlock } from "./common/utils"; 5 | import { PlotOptions, PluginSettings } from "./common/types"; 6 | import { 7 | DEFAULT_PLOT_OPTIONS, 8 | DEFAULT_PLUGIN_SETTINGS, 9 | } from "./common/defaults"; 10 | 11 | export default class ObsidianFunctionPlot extends Plugin { 12 | settings: PluginSettings; 13 | 14 | async onload(): Promise { 15 | // load settings 16 | await this.loadSettings(); 17 | 18 | // add settings tab 19 | this.addSettingTab(new SettingsTab(this.app, this)); 20 | // register command for PlotModal 21 | this.addCommand({ 22 | id: "insert-functionplot", 23 | name: "Plot a function", 24 | editorCallback: (editor: Editor) => { 25 | new CreatePlotModal(this, editor).open(); 26 | }, 27 | }); 28 | // register code block renderer 29 | this.registerMarkdownCodeBlockProcessor( 30 | "functionplot", 31 | this.createFunctionPlotHandler(this) 32 | ); 33 | } 34 | 35 | async loadSettings() { 36 | this.settings = Object.assign( 37 | {}, 38 | DEFAULT_PLUGIN_SETTINGS, 39 | await this.loadData() 40 | ); 41 | } 42 | 43 | async saveSettings() { 44 | await this.saveData(this.settings); 45 | } 46 | 47 | /** 48 | * A closure creating a code-block handler that also has access to the plugin object 49 | * through the outer function's scope. 50 | * @param plugin The plugin 51 | * @returns The code-block handler 52 | */ 53 | createFunctionPlotHandler(plugin: ObsidianFunctionPlot) { 54 | return async ( 55 | source: string, 56 | el: HTMLElement, 57 | _ctx: MarkdownPostProcessorContext /* eslint-disable-line no-unused-vars, @typescript-eslint/no-unused-vars */ 58 | ) => { 59 | const [header, functions] = parseCodeBlock(source); 60 | const options: PlotOptions = Object.assign( 61 | {}, 62 | DEFAULT_PLOT_OPTIONS, 63 | header, 64 | { functions } 65 | ); 66 | await createPlot(options, el, plugin); 67 | }; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # obsidian-functionplot 2 | 3 | A plugin for displaying mathematical graphs in obsidian.md. 4 | 5 | > ⭐ _Remember to star this plugin on [Github](https://github.com/leonhma/obsidian-functionplot) if you like it!_ 6 | 7 | _This file only contains basic instructions to get you to using this plugin quickly. If you want a more detailed documentation, take a look at the [wiki](https://github.com/leonhma/obsidian-functionplot/wiki)._ 8 | 9 | --- 10 | 11 | # 🔮 How to use 12 | 13 | ### With Command 14 | 15 | Since version `1.1.0` you can create plots via a handy GUI with live-preview functionality. 16 | 17 | 1. Open the command palette and select `Obsidian Functionplot: Plot a Function` 18 | 19 | 2. Adjust the plot to your liking. 20 | 21 | ![Create plot modal](./images/create-modal/light.png#gh-light-mode-only) 22 | ![Create plot modal](./images/create-modal//dark.png#gh-dark-mode-only) 23 | 24 | 3. This will create a coordinate system with bounds `-10 < x < 10, -10 < y < 10` and plot the functions f and g. If you havent disabled it, you can even drag and zoom the graph. 25 | 26 | 28 | 29 | ### With `functionplot` Block 30 | 31 | ```` 32 | ```functionplot 33 | --- 34 | title: string 35 | xLabel: string 36 | yLabel: string 37 | bounds: array[min x, max x, min y, max y] 38 | disableZoom: boolean 39 | grid: boolean 40 | --- 41 | (variable)= 42 | ``` 43 | ```` 44 | 45 | Example: 46 | 47 | ```` 48 | ```functionplot 49 | --- 50 | title: The random graph 51 | xLabel: Time 52 | yLabel: Cost 53 | bounds: [0, 10, 0, 50] 54 | disbaleZoom: 1 55 | grid: true 56 | --- 57 | g(x)=x^PI 58 | f(x)=E+log(x)*2 59 | ``` 60 | ```` 61 | 62 | ## 🧮 Supported Math 63 | 64 | To see the complete list of supported math functions, please check the [wiki](https://github.com/leonhma/obsidian-functionplot/wiki). 65 | 66 | ## ⚙ Plugin Settings 67 | 68 | Since version `1.2.0` there's a dedicated settings page for this plugin. Here you can adjust things like font sizes for the text elements of the plot, line widths and various colors. To access this page, head to the obsidian settings and scroll down the list to 'Community Plugins > Obsidian Functionplot'. 69 | 70 | ![Settings Page](https://github.com/leonhma/obsidian-functionplot/blob/master/images/settings/dark.png) 71 | 72 | > **Note** 73 | > For changes to be applied, Obsidian needs to "re-render" the chart. You can either restart Obsidian, or switch between view modes (eg. Reading mode > Edit mode > Reading mode). 74 | 75 | --- 76 | 77 | ## ❓ Questions 78 | 79 | If you have any questions about the usage of the plugin, take a look at the [wiki](https://github.com/leonhma/obsidian-functionplot/wiki) or post a question in the [discussions](https://github.com/leonhma/obsidian-functionplot/discussions). 80 | 81 | ## 🐞 Bugs and Errors 82 | 83 | If you encounter any errors while using this plugin, please report them to us. To do so, click [this link](https://github.com/leonhma/obsidian-functionplot/issues/new?assignees=leonhma&labels=bug&template=BUG_REPORT.yml), fill out the form as best as you can and click `Submit new issue`. These issues are publically viewable, so please don't submit any personal information. 84 | 85 | ## 🤝 Contributing 86 | 87 | Contributions are always welcome! Be it submitting issues, editing the wiki or creating a pull request, contributions by people like you help keep the project evolving. Please adhere to the [contributing guidelines](CONTRIBUTING.md). 88 | 89 | ## ©️ Attribution 90 | 91 | This plugin is based on / uses: 92 | 93 | - [function-plot](https://github.com/mauriciopoppe/function-plot): MIT License, Copyright (c) 2015 Mauricio Poppe 94 | -------------------------------------------------------------------------------- /src/common/utils.ts: -------------------------------------------------------------------------------- 1 | import { Editor, parseYaml } from "obsidian"; 2 | import type ObsidianFunctionPlot from "../main"; 3 | import { PlotOptions, Line } from "./types"; 4 | import { type FunctionPlotOptions } from "function-plot/dist/types"; 5 | import createStylingPlugin from "../plugins/styling"; 6 | import functionPlot, { Chart } from "function-plot"; 7 | 8 | /** 9 | * Insert the text as a new paragraph (newline before and after), and place the active cursor below. 10 | * @param editor The editor element 11 | * @param text The text to place 12 | */ 13 | export function insertParagraphAtCursor( 14 | plugin: ObsidianFunctionPlot, 15 | editor: Editor, 16 | text: string 17 | ) { 18 | // TODO broken 19 | text = "\n" + text.trim() + "\n\n"; 20 | editor.setLine(editor.getCursor().line, text); 21 | } 22 | 23 | /** 24 | * Insert an interactive plot at the current cursor position. 25 | * @param plugin A reference to the plugin 26 | * @param editor A reference to the active editor 27 | * @param options The options for the plot 28 | */ 29 | export function renderPlotAsInteractive( 30 | plugin: ObsidianFunctionPlot, 31 | editor: Editor, 32 | options: PlotOptions 33 | ) { 34 | const text = `\`\`\`functionplot 35 | --- 36 | title: ${options.title} 37 | xLabel: ${options.xLabel} 38 | yLabel: ${options.yLabel} 39 | bounds: [${options.bounds.join(",")}] 40 | disableZoom: ${options.disableZoom} 41 | grid: ${options.grid} 42 | --- 43 | ${(options.functions ?? []).join("\n")} 44 | \`\`\``; 45 | insertParagraphAtCursor(plugin, editor, text); 46 | } 47 | 48 | /** 49 | * Create a plot in the specified `target` element. 50 | * @param options The options for the plot 51 | * @param target The html element to target 52 | * @param plugin A reference to the plugin (accessed for settings) 53 | * @returns The chart object of the created plot 54 | */ 55 | export async function createPlot( 56 | options: PlotOptions, 57 | target: HTMLElement, 58 | plugin: ObsidianFunctionPlot 59 | ): Promise { 60 | try { 61 | const fPlotOptions: FunctionPlotOptions = { 62 | target: target, 63 | plugins: [createStylingPlugin(plugin)], 64 | title: options.title, 65 | grid: options.grid, 66 | disableZoom: options.disableZoom, 67 | xAxis: { 68 | domain: options.bounds.slice(0, 2), 69 | label: options.xLabel, 70 | }, 71 | yAxis: { 72 | domain: options.bounds.slice(2, 4), 73 | label: options.yLabel, 74 | }, 75 | data: options.functions.map((line) => { 76 | // use polyline by default 77 | const lineProperties: Line = {graphType: "polyline"}; 78 | 79 | line.split("@").forEach((property) => { 80 | const tup = property.split("="); 81 | const value = tup[1].trim() 82 | // Using JSON.parse here to convert "range" value from string to real array 83 | lineProperties[tup[0].trim()] = value.startsWith("[") ? JSON.parse(value) : value; 84 | }); 85 | 86 | return lineProperties; 87 | }), 88 | }; 89 | const plot = functionPlot(fPlotOptions); 90 | 91 | return plot; 92 | } catch (e) { 93 | console.debug(e); 94 | } 95 | } 96 | 97 | /** 98 | * Render the plot as an image element using a data url. 99 | * @param plugin A reference to the plugin 100 | * @param editor A reference to the active editor 101 | * @param options The options for the plot 102 | export async function renderPlotAsImage( 103 | plugin: ObsidianFunctionPlot, 104 | editor: Editor, 105 | options: PlotOptions 106 | ) { 107 | const htmlTarget = document.createElement("div"); 108 | await createPlot(options, htmlTarget, plugin); 109 | const dataURL = await toPng(htmlTarget); 110 | if (dataURL === "data:,") { 111 | new Error("Data URL is empty"); 112 | } 113 | const text = `Obsidian Functionplot Plot. Name: ${
114 |     options.title
115 |   }, X-Label: ${options.xLabel}, Y-Label: ${options.yLabel}, Functions: ${(
116 |     options.functions ?? []
117 |   ).join(`; 118 | htmlTarget.remove(); 119 | insertParagraphAtCursor(plugin, editor, text); 120 | } 121 | */ 122 | 123 | export function parseCodeBlock(content: string): [object, string[]] { 124 | let header = {}, 125 | offset = 0; 126 | if (/-{3}[^]*-{3}/.test(content)) { 127 | // header present 128 | const headerMatch = content.match(/-{3}([^]*?)-{3}/)[1]; 129 | offset = headerMatch.length + 6; 130 | header = parseYaml(headerMatch); 131 | } 132 | const functions = content 133 | .slice(offset) 134 | .split("\n") 135 | .map((line) => line.trim()) 136 | .filter((line) => line.length > 0); 137 | return [header, functions]; 138 | } 139 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | yarn-error.log 3 | main.js 4 | data.json 5 | package-lock.json 6 | .nyc_output 7 | coverage 8 | .vscode 9 | .yarn 10 | 11 | # Created by https://www.toptal.com/developers/gitignore/api/node,obsidian,windows,macos,linux,yarn 12 | # Edit at https://www.toptal.com/developers/gitignore?templates=node,obsidian,windows,macos,linux,yarn 13 | 14 | ### Linux ### 15 | *~ 16 | 17 | # temporary files which can be created if a process still has a handle open of a deleted file 18 | .fuse_hidden* 19 | 20 | # KDE directory preferences 21 | .directory 22 | 23 | # Linux trash folder which might appear on any partition or disk 24 | .Trash-* 25 | 26 | # .nfs files are created when an open file is removed but is still being accessed 27 | .nfs* 28 | 29 | ### macOS ### 30 | # General 31 | .DS_Store 32 | .AppleDouble 33 | .LSOverride 34 | 35 | # Icon must end with two \r 36 | Icon 37 | 38 | 39 | # Thumbnails 40 | ._* 41 | 42 | # Files that might appear in the root of a volume 43 | .DocumentRevisions-V100 44 | .fseventsd 45 | .Spotlight-V100 46 | .TemporaryItems 47 | .Trashes 48 | .VolumeIcon.icns 49 | .com.apple.timemachine.donotpresent 50 | 51 | # Directories potentially created on remote AFP share 52 | .AppleDB 53 | .AppleDesktop 54 | Network Trash Folder 55 | Temporary Items 56 | .apdisk 57 | 58 | ### macOS Patch ### 59 | # iCloud generated files 60 | *.icloud 61 | 62 | ### Node ### 63 | # Logs 64 | logs 65 | *.log 66 | npm-debug.log* 67 | yarn-debug.log* 68 | yarn-error.log* 69 | lerna-debug.log* 70 | .pnpm-debug.log* 71 | 72 | # Diagnostic reports (https://nodejs.org/api/report.html) 73 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 74 | 75 | # Runtime data 76 | pids 77 | *.pid 78 | *.seed 79 | *.pid.lock 80 | 81 | # Directory for instrumented libs generated by jscoverage/JSCover 82 | lib-cov 83 | 84 | # Coverage directory used by tools like istanbul 85 | coverage 86 | *.lcov 87 | 88 | # nyc test coverage 89 | .nyc_output 90 | 91 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 92 | .grunt 93 | 94 | # Bower dependency directory (https://bower.io/) 95 | bower_components 96 | 97 | # node-waf configuration 98 | .lock-wscript 99 | 100 | # Compiled binary addons (https://nodejs.org/api/addons.html) 101 | build/Release 102 | 103 | # Dependency directories 104 | node_modules/ 105 | jspm_packages/ 106 | 107 | # Snowpack dependency directory (https://snowpack.dev/) 108 | web_modules/ 109 | 110 | # TypeScript cache 111 | *.tsbuildinfo 112 | 113 | # Optional npm cache directory 114 | .npm 115 | 116 | # Optional eslint cache 117 | .eslintcache 118 | 119 | # Optional stylelint cache 120 | .stylelintcache 121 | 122 | # Microbundle cache 123 | .rpt2_cache/ 124 | .rts2_cache_cjs/ 125 | .rts2_cache_es/ 126 | .rts2_cache_umd/ 127 | 128 | # Optional REPL history 129 | .node_repl_history 130 | 131 | # Output of 'npm pack' 132 | *.tgz 133 | 134 | # Yarn Integrity file 135 | .yarn-integrity 136 | 137 | # dotenv environment variable files 138 | .env 139 | .env.development.local 140 | .env.test.local 141 | .env.production.local 142 | .env.local 143 | 144 | # parcel-bundler cache (https://parceljs.org/) 145 | .cache 146 | .parcel-cache 147 | 148 | # Next.js build output 149 | .next 150 | out 151 | 152 | # Nuxt.js build / generate output 153 | .nuxt 154 | dist 155 | 156 | # Gatsby files 157 | .cache/ 158 | # Comment in the public line in if your project uses Gatsby and not Next.js 159 | # https://nextjs.org/blog/next-9-1#public-directory-support 160 | # public 161 | 162 | # vuepress build output 163 | .vuepress/dist 164 | 165 | # vuepress v2.x temp and cache directory 166 | .temp 167 | 168 | # Docusaurus cache and generated files 169 | .docusaurus 170 | 171 | # Serverless directories 172 | .serverless/ 173 | 174 | # FuseBox cache 175 | .fusebox/ 176 | 177 | # DynamoDB Local files 178 | .dynamodb/ 179 | 180 | # TernJS port file 181 | .tern-port 182 | 183 | # Stores VSCode versions used for testing VSCode extensions 184 | .vscode-test 185 | 186 | # yarn v2 187 | .yarn/cache 188 | .yarn/unplugged 189 | .yarn/build-state.yml 190 | .yarn/install-state.gz 191 | .pnp.* 192 | 193 | ### Node Patch ### 194 | # Serverless Webpack directories 195 | .webpack/ 196 | 197 | # Optional stylelint cache 198 | 199 | # SvelteKit build / generate output 200 | .svelte-kit 201 | 202 | ### Obsidian ### 203 | # config dir 204 | .obsidian/ 205 | 206 | ### Windows ### 207 | # Windows thumbnail cache files 208 | Thumbs.db 209 | Thumbs.db:encryptable 210 | ehthumbs.db 211 | ehthumbs_vista.db 212 | 213 | # Dump file 214 | *.stackdump 215 | 216 | # Folder config file 217 | [Dd]esktop.ini 218 | 219 | # Recycle Bin used on file shares 220 | $RECYCLE.BIN/ 221 | 222 | # Windows Installer files 223 | *.cab 224 | *.msi 225 | *.msix 226 | *.msm 227 | *.msp 228 | 229 | # Windows shortcuts 230 | *.lnk 231 | 232 | ### yarn ### 233 | # https://yarnpkg.com/getting-started/qa#which-files-should-be-gitignored 234 | 235 | .yarn/* 236 | !.yarn/releases 237 | !.yarn/patches 238 | !.yarn/plugins 239 | !.yarn/sdks 240 | !.yarn/versions 241 | 242 | # if you are NOT using Zero-installs, then: 243 | # comment the following lines 244 | !.yarn/cache 245 | 246 | # and uncomment the following lines 247 | # .pnp.* 248 | 249 | # End of https://www.toptal.com/developers/gitignore/api/node,obsidian,windows,macos,linux,yarn 250 | -------------------------------------------------------------------------------- /src/app/CreatePlotModal.ts: -------------------------------------------------------------------------------- 1 | import { Chart } from "function-plot"; 2 | import { FunctionPlotDatum } from "function-plot/dist/types"; 3 | import { Editor, Modal, Setting } from "obsidian"; 4 | import { DEFAULT_PLOT_OPTIONS } from "../common/defaults"; 5 | import { PlotOptions, rendererType, Line } from "../common/types"; 6 | import type ObsidianFunctionPlot from "../main"; 7 | import { createPlot, renderPlotAsInteractive } from "../common/utils"; 8 | 9 | export default class CreatePlotModal extends Modal { 10 | options: PlotOptions; 11 | plugin: ObsidianFunctionPlot; 12 | editor: Editor; 13 | plot: Chart; 14 | renderer: rendererType; 15 | 16 | constructor(plugin: ObsidianFunctionPlot, editor: Editor) { 17 | super(plugin.app); 18 | this.plugin = plugin; 19 | this.editor = editor; 20 | this.renderer = this.plugin.settings.defaultRenderer; 21 | } 22 | 23 | /** 24 | * Reload the preview using internal functions. Zooming doesn't work here. 25 | * @returns A Promise 26 | */ 27 | reloadPreview() { 28 | if (!this.plot) return; 29 | // update values 30 | this.plot.options.title = this.options.title; 31 | this.plot.options.xAxis.label = this.options.xLabel; 32 | this.plot.options.yAxis.label = this.options.yLabel; 33 | this.plot.options.xAxis.domain = this.options.bounds.slice(0, 2); 34 | this.plot.options.yAxis.domain = this.options.bounds.slice(2, 4); 35 | this.plot.options.grid = this.options.grid; 36 | this.plot.options.data = this.options.functions.map( 37 | (line): FunctionPlotDatum => { 38 | // use polyline by default 39 | const lineProperties: Line = {graphType: "polyline"}; 40 | 41 | line.split("@").forEach((property) => { 42 | const tup = property.split("="); 43 | const value = tup[1].trim() 44 | lineProperties[tup[0].trim()] = value.startsWith("[") ? JSON.parse(value) : value; 45 | }); 46 | 47 | return lineProperties; 48 | } 49 | ); 50 | // redirect errors within function-plot to debug 51 | try { 52 | this.plot.build(); 53 | } catch (e) { 54 | console.debug(e); 55 | } 56 | } 57 | 58 | async onOpen() { 59 | this.options = Object.assign({}, DEFAULT_PLOT_OPTIONS); 60 | 61 | const { contentEl } = this; 62 | contentEl.empty(); 63 | 64 | // Header 65 | contentEl.createEl("h1", { text: "Plot a function" }); 66 | 67 | const flex = contentEl.createDiv({ 68 | attr: { style: "display: flex; align-items: center" }, 69 | }); 70 | 71 | const settings = flex.createDiv(); 72 | const preview = flex.createDiv({ attr: { style: "padding: 1em" } }); 73 | this.plot = await createPlot( 74 | Object.assign({}, this.options, { disableZoom: true }), 75 | preview.createDiv(), 76 | this.plugin 77 | ); 78 | preview.createEl("p", { 79 | text: "Preview - Zoom is disabled while in preview", 80 | attr: { 81 | style: "margin: 0 3em; font-size: 0.8em; color: var(--text-faint)", 82 | }, 83 | }); 84 | 85 | new Setting(settings).setName("Title").addText((text) => { 86 | text.onChange(async (value) => { 87 | this.options.title = value; 88 | this.reloadPreview(); 89 | }); 90 | }); 91 | 92 | new Setting(settings).setName("Label X").addText((text) => { 93 | text.onChange(async (value) => { 94 | this.options.xLabel = value; 95 | this.reloadPreview(); 96 | }); 97 | }); 98 | new Setting(settings).setName("Label Y").addText((text) => { 99 | text.onChange(async (value) => { 100 | this.options.yLabel = value; 101 | this.reloadPreview(); 102 | }); 103 | }); 104 | 105 | new Setting(settings) 106 | .setName("Bounds") 107 | .setDesc("Bounds must be written in this format: minX, maxX, minY, maxY.") 108 | .addText((text) => { 109 | text.setPlaceholder(DEFAULT_PLOT_OPTIONS.bounds.join(", ")); 110 | text.onChange(async () => { 111 | let bounds = text 112 | .getValue() 113 | .split(",") 114 | .map((c) => parseFloat(c)); 115 | const n = bounds.filter((v) => !isNaN(v)).length; 116 | if (n === 0) { 117 | bounds = DEFAULT_PLOT_OPTIONS.bounds; 118 | } 119 | if (n >= 4 || n === 0) { 120 | this.options.bounds = bounds as PlotOptions["bounds"]; 121 | this.reloadPreview(); 122 | } 123 | }); 124 | }); 125 | 126 | new Setting(settings).setName("Disable Zoom").addToggle((com) => { 127 | com.setValue(this.options.disableZoom); 128 | com.onChange(async (value) => { 129 | this.options.disableZoom = value; 130 | this.reloadPreview(); 131 | }); 132 | }); 133 | 134 | new Setting(settings).setName("Enable Grid").addToggle((com) => { 135 | com.setValue(this.options.grid); 136 | com.onChange(async (value) => { 137 | this.options.grid = value; 138 | this.reloadPreview(); 139 | }); 140 | }); 141 | 142 | new Setting(settings) 143 | .setName("Functions") 144 | .setDesc( 145 | "Specify functions to plot. Must be in format: (x) = ." 146 | ) 147 | .addTextArea((com) => { 148 | com.onChange(async (value) => { 149 | if (!value.trim()) return; 150 | this.options.functions = value 151 | .split("\n") 152 | .map((f) => f.trim() || undefined); 153 | // maybe check if there are valid functions 154 | this.reloadPreview(); 155 | }); 156 | }); 157 | 158 | new Setting(contentEl) 159 | /*.addDropdown((com) => { 160 | com 161 | .addOptions(rendererOptions) 162 | .setValue(this.plugin.settings.defaultRenderer) 163 | .onChange((value: rendererType) => { 164 | this.renderer = value; 165 | }); 166 | })*/ 167 | .addButton((btn) => { 168 | btn 169 | .setButtonText("Plot") 170 | .setCta() 171 | .onClick(async () => { 172 | await this.handlePlotCreate(this.options); 173 | }); 174 | }); 175 | } 176 | 177 | async handlePlotCreate(options: PlotOptions) { 178 | // render and insert chosen plot using renderer 179 | switch (this.renderer) { 180 | case "interactive": 181 | renderPlotAsInteractive(this.plugin, this.editor, options); 182 | break; 183 | /* 184 | case "image": 185 | await renderPlotAsImage(this.plugin, this.editor, options); 186 | break; */ 187 | } 188 | 189 | this.close(); 190 | } 191 | 192 | onClose() { 193 | const { contentEl } = this; 194 | contentEl.empty(); 195 | this.plot = null; 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /src/app/SettingsTab.ts: -------------------------------------------------------------------------------- 1 | import { 2 | App, 3 | Notice, 4 | PluginSettingTab, 5 | Setting, 6 | ValueComponent, 7 | } from "obsidian"; 8 | import type ObsidianFunctionPlot from "../main.js"; 9 | import { DEFAULT_PLUGIN_SETTINGS } from "../common/defaults"; 10 | import { PluginSettings } from "../common/types"; 11 | 12 | export default class SettingsTab extends PluginSettingTab { 13 | plugin: ObsidianFunctionPlot; 14 | settingsInputs: Map>; 15 | 16 | constructor(app: App, plugin: ObsidianFunctionPlot) { 17 | super(app, plugin); 18 | this.plugin = plugin; 19 | } 20 | 21 | display() { 22 | this.settingsInputs = new Map(); 23 | const { containerEl } = this; 24 | containerEl.empty(); 25 | 26 | containerEl.createEl("h1", { text: "Settings" }); 27 | 28 | /* 29 | * Default Plot Options 30 | */ 31 | 32 | containerEl.createEl("h3", { text: "Default Plot Options" }); 33 | /* 34 | new Setting(containerEl) 35 | .setName("Default Render Type") 36 | .setDesc("The default renderer to use.") 37 | .addDropdown((com) => { 38 | this.settingsInputs.set("defaultRenderer", com); 39 | com 40 | .addOptions(rendererOptions) 41 | .setValue(this.plugin.settings.defaultRenderer) 42 | .onChange(async (value: rendererType) => { 43 | this.plugin.settings.defaultRenderer = value; 44 | await this.plugin.saveSettings(); 45 | }); 46 | }); 47 | */ 48 | /* 49 | * Font Sizes 50 | */ 51 | 52 | containerEl.createEl("h3", { text: "Font Sizes" }); 53 | 54 | new Setting(containerEl) 55 | .setName("Title Font Size") 56 | .setDesc("Font size used for the title.") 57 | .addSlider((slider) => { 58 | this.settingsInputs.set("titleFontSize", slider); 59 | slider 60 | .setLimits(8, 40, 2) 61 | .setValue(this.plugin.settings.titleFontSize) 62 | .onChange(async (value) => { 63 | this.plugin.settings.titleFontSize = value; 64 | await this.plugin.saveSettings(); 65 | }) 66 | .setDynamicTooltip(); 67 | }); 68 | 69 | new Setting(containerEl) 70 | .setName("Scale Font Size") 71 | .setDesc("Font size used for the axis scales.") 72 | .addSlider((slider) => { 73 | this.settingsInputs.set("scaleFontSize", slider); 74 | slider 75 | .setLimits(4, 20, 1) 76 | .setValue(this.plugin.settings.scaleFontSize) 77 | .onChange(async (value) => { 78 | this.plugin.settings.scaleFontSize = value; 79 | await this.plugin.saveSettings(); 80 | }) 81 | .setDynamicTooltip(); 82 | }); 83 | 84 | new Setting(containerEl) 85 | .setName("Label Font Size") 86 | .setDesc("Font size used for the axis labels.") 87 | .addSlider((slider) => { 88 | this.settingsInputs.set("labelFontSize", slider); 89 | slider 90 | .setLimits(4, 20, 1) 91 | .setValue(this.plugin.settings.labelFontSize) 92 | .onChange(async (value) => { 93 | this.plugin.settings.labelFontSize = value; 94 | await this.plugin.saveSettings(); 95 | }) 96 | .setDynamicTooltip(); 97 | }); 98 | 99 | /* 100 | * Line Widths 101 | */ 102 | 103 | containerEl.createEl("h3", { text: "Line Widths" }); 104 | 105 | new Setting(containerEl) 106 | .setName("Line Width") 107 | .setDesc("Line width used for the domain- and origin-lines.") 108 | .addSlider((slider) => { 109 | this.settingsInputs.set("lineWidth", slider); 110 | slider 111 | .setLimits(1, 4, 1) 112 | .setValue(this.plugin.settings.lineWidth) 113 | .onChange(async (value) => { 114 | this.plugin.settings.lineWidth = value; 115 | await this.plugin.saveSettings(); 116 | }) 117 | .setDynamicTooltip(); 118 | }); 119 | 120 | new Setting(containerEl) 121 | .setName("Grid Line Width") 122 | .setDesc("Line width used for the gridlines.") 123 | .addSlider((slider) => { 124 | this.settingsInputs.set("gridWidth", slider); 125 | slider 126 | .setLimits(1, 4, 1) 127 | .setValue(this.plugin.settings.gridWidth) 128 | .onChange(async (value) => { 129 | this.plugin.settings.gridWidth = value; 130 | await this.plugin.saveSettings(); 131 | }) 132 | .setDynamicTooltip(); 133 | }); 134 | 135 | /* 136 | * Colors 137 | */ 138 | 139 | containerEl.createEl("h3", { 140 | attr: { style: "margin-bottom: 0" }, 141 | text: "Colors", 142 | }); 143 | containerEl.createEl("p", { 144 | attr: { 145 | style: "margin-top: 8px; font-size: 0.8em; color: var(--text-faint)", 146 | }, 147 | text: "Use any of the web formats (name, hex, rgb, rgba, ...) or css variables.", 148 | }); 149 | 150 | new Setting(containerEl) 151 | .setName("Font Color") 152 | .setDesc("Color used for the title and labels.") 153 | .addText((text) => { 154 | this.settingsInputs.set("fontColor", text); 155 | text 156 | .setValue(this.plugin.settings.fontColor) 157 | .onChange(async (value) => { 158 | this.plugin.settings.fontColor = value; 159 | await this.plugin.saveSettings(); 160 | }); 161 | }); 162 | 163 | new Setting(containerEl) 164 | .setName("Line Color") 165 | .setDesc("Color used for the domain- and origin-lines.") 166 | .addText((text) => { 167 | this.settingsInputs.set("lineColor", text); 168 | text 169 | .setValue(this.plugin.settings.lineColor) 170 | .onChange(async (value) => { 171 | this.plugin.settings.lineColor = value; 172 | await this.plugin.saveSettings(); 173 | }); 174 | }); 175 | 176 | new Setting(containerEl) 177 | .setName("Grid Color") 178 | .setDesc("Color used for the gridlines.") 179 | .addText((text) => { 180 | this.settingsInputs.set("gridColor", text); 181 | text 182 | .setValue(this.plugin.settings.gridColor) 183 | .onChange(async (value) => { 184 | this.plugin.settings.gridColor = value; 185 | await this.plugin.saveSettings(); 186 | }); 187 | }); 188 | 189 | /* 190 | * Reset Settings 191 | */ 192 | 193 | new Setting(containerEl).addButton((btn) => { 194 | btn 195 | .setButtonText("Reset Settings to Default") 196 | .setWarning() 197 | .onClick(() => { 198 | Object.assign(this.plugin.settings, DEFAULT_PLUGIN_SETTINGS); 199 | for (const [key, input] of this.settingsInputs) { 200 | if (key === "telemetry") continue; // don't reset telemetry 201 | input.setValue(this.plugin.settings[key]); 202 | } 203 | this.plugin.saveSettings(); 204 | // skipcq: JS-0078 205 | new Notice("Settings reset to default"); 206 | }); 207 | }); 208 | } 209 | } 210 | --------------------------------------------------------------------------------