diff --git a/helpers/graphs.ts b/helpers/graphs.ts index bd3cd83..5fc691a 100644 --- a/helpers/graphs.ts +++ b/helpers/graphs.ts @@ -3,10 +3,10 @@ import Chart, { ChartConfiguration, ChartType } from "chart.js/auto"; import zoomPlugin from "chartjs-plugin-zoom"; - +import { customNotice } from "main"; import {PlotData,Data,LineProperties, findPossibleProperty} from "./parser" -import { validLineProperties } from "./plotProperties"; +import { validBarDatasetProperties, validLineDatasetProperties } from "./plotProperties"; type Dataset = { label: string, @@ -17,18 +17,28 @@ type Dataset = { Chart.register(zoomPlugin as any); export function createPlot(canvas: HTMLCanvasElement, data: PlotData[], parsedMd: LineProperties[], plotProperties: ChartConfiguration["options"], chartType: ChartType ) { + if (chartType === "pie" || chartType === "doughnut" || chartType === "polarArea" || chartType === "radar"){ + const circularData = transformData(data,chartType); + if (circularData === undefined) return; + return new Chart(canvas, { + type: chartType, + data: circularData, + options: { + ...plotProperties, + }}); + } else { + const datasets = buildDatasets(data,parsedMd); + return new Chart(canvas, { + type: chartType, + data: { + datasets: datasets}, + options: { - const datasets = buildDatasets(data,parsedMd); - return new Chart(canvas, { - type: chartType, - data: { - datasets: datasets}, - options: { + ...plotProperties, + }}); + }; - ...plotProperties, - } - }); } export function buildDatasets(data: PlotData[], parsedMd: LineProperties[]) { @@ -44,7 +54,7 @@ export function buildDatasets(data: PlotData[], parsedMd: LineProperties[]) { const props = parsedMd.filter(p => p.signature === eq.signature); props.forEach(p => { - if (validLineProperties.includes(p.property)) { + if (validLineDatasetProperties.includes(p.property) || validBarDatasetProperties.includes(p.property)) { dataset[p.property] = p.value; if (p.property === "borderColor") { @@ -55,7 +65,7 @@ export function buildDatasets(data: PlotData[], parsedMd: LineProperties[]) { } } else { - findPossibleProperty(p.property,validLineProperties,"LineProperty"); + findPossibleProperty(p.property,[...validLineDatasetProperties,...validBarDatasetProperties],"LineProperty"); } }); @@ -63,6 +73,43 @@ export function buildDatasets(data: PlotData[], parsedMd: LineProperties[]) { }); } + +function transformData(data: PlotData[], chartType: ChartType){ + switch(chartType){ + case "pie": + case "polarArea": + if (data.length > 1) { + customNotice("Pie and Polar Area charts are meant for 1 dataset. Use doughnut for multiple"); + }; + const first = data[0]; + if (!first) return {labels: [], datasets: []}; + return { + labels: first.data.map(p => p.x), + datasets: [ + { + label: first.signature, + data: first.data.map(p => p.y) + } + ] + }; + case "doughnut": + case "radar": + const labels = data[0]?.data.map(p=> p.x) ?? []; + return { + labels, + datasets: data.map(ds => ({ + label: ds.signature, + data: labels.map(label => { + const found = ds.data.find(p => p.x === label); + return found ? found.y : null; + }) + })) + }; + default: + return undefined; + } +} + const getRandomRGB = (): string => { const r = Math.floor(Math.random() * 256); const g = Math.floor(Math.random() * 256); diff --git a/helpers/parser.ts b/helpers/parser.ts index 40eaa67..cd8ec4b 100644 --- a/helpers/parser.ts +++ b/helpers/parser.ts @@ -7,11 +7,11 @@ import { Notice, App, TFile} from "obsidian"; import { getApp } from "./appContext"; import { customNotice,isTuple } from "main"; -import { validLineProperties } from "./plotProperties"; +import { validLineDatasetProperties,validBarDatasetProperties } from "./plotProperties"; import { min } from "mathjs"; import { string } from "mathjs"; import { isArray } from "chart.js/dist/helpers/helpers.core"; -import {validPlotProperties,validRoots} from "./plotProperties" +import {validObjProperties,validRoots} from "./plotProperties" import { sign } from "mathjs"; const math = create(all); @@ -104,7 +104,11 @@ export async function handleMarkdown(markdown: string[], defaultProperties: Char } return parsedText; } - case "bar": { + case "bar": + case "pie": + case "doughnut": + case "polarArea": + case "radar": { return { lineProperties: handleLineProperties(lines.filter(s => (!s.includes("obj.") || !s.includes("global.")) && propertyPattern.test(s)),propertyPattern), chartOptions: handlePlotProperties(lines.filter(s => s.startsWith("obj.")), defaultProperties), @@ -246,12 +250,12 @@ export function handlePlotProperties(lines: string[], defaultProperties: ChartCo if (key !== undefined && value !== undefined) { //Type Narrowing const last = key.split(".").pop() ?? "" - if (validPlotProperties.includes(last)) { // validPlotProperties doesnt include x or y or etc just title + if (validObjProperties.includes(last)) { // validPlotProperties doesnt include x or y or etc just title helperPlotProperties(properties,key,value); } else { // Evaluate using fuzzy matching later - findPossibleProperty(key,validPlotProperties,"PlotProperties",validRoots); + findPossibleProperty(key,validObjProperties,"PlotProperties",validRoots); // Throw an error later } } @@ -674,7 +678,7 @@ export function findPossibleProperty(key: string, validProps: string[], flag: st case "LineProperty": { let bestMatch = ""; let bestDist = Infinity; - for (let prop of validLineProperties) { + for (let prop of validProps) { const dist = levD(key, prop); if (dist < bestDist) { bestDist = dist; @@ -720,8 +724,8 @@ export function findPossibleProperty(key: string, validProps: string[], flag: st const k = keyPath[p]; if (k === undefined) continue; - if (!validPlotProperties.includes(k)) { - for (let prop of validPlotProperties) { + if (!validProps.includes(k)) { + for (let prop of validProps) { const dist = levD(k,prop); if (dist < bestDist) { bestDist = dist; diff --git a/helpers/plotProperties.ts b/helpers/plotProperties.ts index c2a8e32..3ce39f7 100644 --- a/helpers/plotProperties.ts +++ b/helpers/plotProperties.ts @@ -1,5 +1,5 @@ -import { CompletionContext } from "@codemirror/autocomplete"; +import { Completion, CompletionContext } from "@codemirror/autocomplete"; import { PlotPluginSettings } from "settings"; import { getDefaultPlotProperties } from "main"; import LinearScaleBase from "chart.js/dist/scales/scale.linearbase"; @@ -17,51 +17,69 @@ export const validRoots = [ "animation" ] -export const validPlotProperties = [ - "type", - "min", - "max", - "display", - "position", - "text", - "color", - "stepSize", - "beginAtZero", - "suggestedMin", - "suggestedMax", - "lineWidth", - "drawBorder", - "drawOnChartArea", - "drawTicks", - "tickLength", - "mode", - "intersect", - "enabled", - "borderColor", - "backgroundColor", - "borderWidth", - "pointRadius", - "tension", - "fill", - "hidden" +export const validObjProperties = [ + "type", + "min", + "max", + "display", + "position", + "text", + "color", + + "stepSize", + "beginAtZero", + "suggestedMin", + "suggestedMax", + + "lineWidth", + "drawBorder", + "drawOnChartArea", + "drawTicks", + "tickLength", + + "mode", + "intersect", + "enabled", + + "responsive", + "maintainAspectRatio", + "animation", + + "padding", + + "stacked", + "offset", + + "indexAxis", + + "borderColor", + "backgroundColor", + "borderWidth", + + "pointRadius", + "tension", + "fill", + + "hidden" ]; -export const validLineProperties = [ - "backgroundColor", - "borderCapStyle", - "borderColor", - "borderDash", - "borderDashOffset", - "borderJoinStyle", - "borderWidth", - "fill", - "tension", - "showLine", - "spanGaps", - "xrange", - "pointStyle", - "pointRadius", - "label", +export const validLineDatasetProperties = [ + "label", + "backgroundColor", + "borderCapStyle", + "borderColor", + "borderDash", + "borderDashOffset", + "borderJoinStyle", + "borderWidth", + "fill", + "tension", + "showLine", + "spanGaps", + "xrange", + "pointStyle", + "pointRadius", + "hidden" ] export const globalOptions = [ @@ -75,21 +93,137 @@ export const globalOptions = [ ] -export const barStyleProperties = [ - "backgroundColor" -] +export const validBarDatasetProperties = [ + "label", + "backgroundColor", + "borderColor", + "borderWidth", + "borderRadius", + "borderSkipped", + + "barPercentage", + "categoryPercentage", + + "base", + "inflateAmount", + "maxBarThickness", + "minBarLength", + + "order", + "stack", + "grouped", + + "hoverBackgroundColor", + "hoverBorderColor", + "hoverBorderWidth", + + "hidden" +]; const propertyPattern = /^\s*(.+?)\.([a-zA-Z_]\w*)?\s*/; +const propertyDescriptions: Record = { + xrange: "Controls the range for evaluating a main equation.", + canvasWidth: "Controls the chart width in pixels.", + canvasHeight: "Controls the chart height in pixels.", + canvasRadius: "Controls chart corner radius.", + canvasBackground: "Controls the color of the chart background.", + canvasMargin: "Controls the margins of the chart in pixels.", + canvasPadding: "Controls chart padding in pixels.", + + backgroundColor: "Main color option. Fills line, bar, pie, points, and areas.", + borderColor: "Controls the border or stroke color.", + borderWidth: "Controls border or line thickness.", + borderCapStyle: "Controls the cap style of line ends.", + borderDash: "Controls dashed line pattern.", + borderDashOffset: "Controls dash starting offset.", + borderJoinStyle: "Controls how line corners are joined.", + borderRadius: "Rounds bar, arc, or element corners.", + borderSkipped: "Skips drawing borders on selected sides.", + + fill: "Fills the area under a line or radar dataset.", + tension: "Controls line curve smoothness. 0 = straight lines.", + stepped: "Draws stepped lines instead of diagonal segments.", + showLine: "Shows connecting lines between points.", + spanGaps: "Connects lines across missing or null values.", + + pointRadius: "Controls point size.", + pointHoverRadius: "Controls point size while hovered.", + pointHitRadius: "Controls clickable area around points.", + pointStyle: "Controls point shape.", + radius: "Controls radius of arcs, bubbles, or points.", + hoverRadius: "Controls radius while hovered.", + hoverOffset: "Moves pie or doughnut slices outward on hover.", + + barThickness: "Sets fixed bar thickness.", + maxBarThickness: "Sets maximum allowed bar thickness.", + minBarLength: "Sets minimum visible bar length.", + categoryPercentage: "Controls category width used by bars.", + barPercentage: "Controls bar width inside category space.", + indexAxis: "Changes chart direction. x = vertical, y = horizontal.", + + cutout: "Controls doughnut hole size.", + rotation: "Controls starting rotation angle.", + circumference: "Controls total sweep angle of pie or doughnut.", + spacing: "Controls spacing between arc segments.", + + responsive: "Resizes chart with container.", + maintainAspectRatio: "Keeps chart aspect ratio when resizing.", + aspectRatio: "Sets preferred width to height ratio.", + + display: "Shows or hides an item.", + text: "Controls displayed text.", + position: "Controls element position.", + align: "Controls alignment.", + color: "Controls text or element color.", + font: "Controls font styling.", + + min: "Sets minimum axis value.", + max: "Sets maximum axis value.", + beginAtZero: "Starts axis at zero.", + suggestedMin: "Suggested minimum axis value.", + suggestedMax: "Suggested maximum axis value.", + reverse: "Reverses axis direction.", + stacked: "Stacks datasets on the axis.", + offset: "Adds spacing before first and after last tick.", + type: "Controls axis scale type.", + labels: "Controls category labels.", + + enabled: "Enables or disables a feature.", + mode: "Controls interaction or display mode.", + intersect: "Requires pointer to intersect element.", + axis: "Controls active interaction axis.", + + plugins: "Container for plugin options.", + legend: "Controls legend settings.", + title: "Controls title settings.", + tooltip: "Controls tooltip settings.", + + scales: "Container for axis settings.", + animation: "Controls chart animation behavior.", + parsing: "Controls automatic data parsing.", + normalized: "Improves performance for sorted data.", + clip: "Clips drawing outside chart area.", + + grid: "Controls axis grid line settings.", + ticks: "Controls axis tick label settings.", + autoSkip: "Automatically hides some tick labels if crowded.", + precision: "Controls decimal precision of tick values.", + + +} type TreeNode = { [key: string]: TreeNode; }; -export function linePlotCompletionSource(settings: PlotPluginSettings, chartType: ChartType) { - const chartTree = objectToTree(getDefaultPlotProperties(settings, chartType)); // Get default properties from main and create a tree object to travers +export function PlotCompletionSource(settings: PlotPluginSettings) { + return function(context: CompletionContext) { // function that autocompletion accepts + const chartType = getChartTypeFromEditor(context); + if (!chartType) return null; + const chartTree = objectToTree(getDefaultPlotProperties(settings, chartType)); // Get default properties from main and create a tree object to travers if (!inLineplotBlock(context)) return null; // if the text doesnt have ```lineplot at some point before, dont autocomplete. Couldnt get syntaxTree to work const match = context.matchBefore(/[a-zA-Z0-9_.()]*/); // Matches text before cursor stop if (!match) return null; @@ -113,7 +247,8 @@ export function linePlotCompletionSource(settings: PlotPluginSettings, chartType from: context.pos - partial.length, // we only replace the property being written not the whole obj. -> path options: Object.keys(node).filter(key => key.startsWith(partial)).map( key => ({ label: key, - type: "function" + type: "function", + info: getPropertyDescription(key) })) }; } @@ -121,12 +256,28 @@ export function linePlotCompletionSource(settings: PlotPluginSettings, chartType else if (propertyPattern.test(match.text) && !match.text.startsWith("obj.") && !match.text.startsWith("global.")) { const dotIndex = match.text.indexOf("."); const replaceText = match.text.slice(dotIndex + 1); // Removes anything before . so f(x).borderColor -> borderColor - + let arr = validLineDatasetProperties; + switch(chartType){ + case "line": + case "scatter": { + arr = validLineDatasetProperties; + break; + } + case "bar": { + arr = validBarDatasetProperties; + break; + } + default: + arr = [...validLineDatasetProperties,...validBarDatasetProperties]; + break; + } return { from: context.pos - replaceText.length, - options: validLineProperties.map(s => ({ + options: arr.map(s => ({ label: s, - type: "function" + type: "function", + info: getPropertyDescription(s) + })), } } @@ -138,7 +289,8 @@ export function linePlotCompletionSource(settings: PlotPluginSettings, chartType options: globalOptions.map(s => ({ label: s, type: "function", - filter: false + filter: false, + info: getPropertyDescription(s) })) } } @@ -182,4 +334,29 @@ export function objectToTree(obj: any): TreeNode { } return tree; +} + +function getPropertyDescription(property: string) { + return propertyDescriptions[property] ?? "No description available."; +} + +function getChartTypeFromEditor(context: CompletionContext): ChartType { + const text = context.state.doc.toString(); + const pos = context.pos; + + const beforeCursor = text.slice(0,pos); + const blockStart = beforeCursor.lastIndexOf("```datachart"); + if (blockStart === -1) return "line"; + + const blockText = beforeCursor.slice(blockStart); + const match = blockText.match(/type::\s*(\w+)/); + if (!match || !match[1]) return "line"; + + const type = match[1].trim().toLocaleLowerCase(); + + if (type === "line" || type === "bar" || type === "scatter" || type === "pie" || type === "doughnut" || type === "radar") { + return type; + } + return "line" + } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index 0bb1f6e..5badcab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -8,9 +8,11 @@ import { PlotPluginSettings, DEFAULT_SETTINGS, PlotSettingTab } from "settings"; import { ChartOptions, ChartType, Ticks } from "chart.js/auto"; import {autocompletion} from "@codemirror/autocomplete"; -import { linePlotCompletionSource } from "../helpers/plotProperties"; +import { PlotCompletionSource } from "../helpers/plotProperties"; import { zoom } from "chartjs-plugin-zoom"; import { divideScalarDependencies } from "mathjs"; +import { codePointSize } from "@codemirror/state"; +import { e } from "mathjs"; export default class PlotPlugin extends Plugin { settings!: PlotPluginSettings; @@ -22,7 +24,7 @@ export default class PlotPlugin extends Plugin { this.registerEditorExtension([ autocompletion({ - override: [linePlotCompletionSource(this.settings,"line")], + override: [PlotCompletionSource(this.settings)], activateOnTyping: true, })] ) @@ -60,6 +62,12 @@ export default class PlotPlugin extends Plugin { case "bar": chartInstance = await this.renderBar(cachedParsedText,chartInstance,el, chartType); break; + case "pie": + case "doughnut": + case "polarArea": + case "radar": + chartInstance = await this.renderCircular(cachedParsedText, chartInstance, el, chartType); + break; } }; @@ -163,6 +171,17 @@ export default class PlotPlugin extends Plugin { ] return await this.createChartInstane(chartInstance,el,globalProperties,data,parsedText, chartType); }; + async renderCircular(cachedParsedText: parsedText, chartInstance: any, el: HTMLElement, chartType: ChartType){ + const parsedText = cachedParsedText; + const globalProperties = parsedText.globalProperties; + const data: PlotData[] = [ + ...parsedText.manualData, + ...parsedText.tableData + ] + console.log(data); + // Manage data to fit how pie and others accept it. + return await this.createChartInstane(chartInstance, el, globalProperties, data, parsedText, chartType); + }; async createChartInstane (chartInstance: any, el: HTMLElement, globalProperties: GlobalProperties[], data: PlotData[], parsedText: parsedText, chartType: ChartType) { @@ -486,6 +505,8 @@ export function getDefaultPlotProperties(settings: PlotPluginSettings, chartType }; } + + export function getTypeDefaults(chartType: ChartType, settings: PlotPluginSettings): ChartOptions { const style = getComputedStyle(document.body); const fontFamily = style.getPropertyValue("--font-interface").trim() || "sans-serif"; @@ -531,13 +552,48 @@ export function getTypeDefaults(chartType: ChartType, settings: PlotPluginSettin case "bar": return { elements: { - bar: {borderWidth: settings.EborderWidth, borderRadius: 4} + bar: {borderWidth: settings.EborderWidth, borderRadius: 4, borderSkipped: false} }, scales: { - x: {type: settings.xScalesType}, - y: {type: settings.yScalesType} - } + x: {type: settings.xScalesType, offset: true, stacked: false, grid: {display:false}, ticks: {autoSkip:false}, + title: {display: !settings.titleStatus, text: ""}}, + y: {type: settings.yScalesType, stacked: false, grid: {display: true}, title: {display: !settings.titleStatus, text: ""}} + }, + indexAxis: "x" }; + case "pie": + return { + elements: {arc: {borderWidth: settings.EborderWidth}}, + radius: "90%", + rotation: 0, + circumference: 360 + } as any; + case "doughnut": + return { + elements: {arc: {borderWidth: settings.EborderWidth}}, + radius: "90%", + cutout: "50%", + rotation: 0, + circumference: 360 + } as any; + case "polarArea": + return { + elements: {arc: {borderWidth: settings.EborderWidth}}, + scales: {r: {beginAtZero: true, ticks: {backdropColor: "transparent"}}} + } as any; + case "radar": + return { + elements: {line: {borderWidth: settings.EborderWidth,tension: 0.15,fill: true}, + point: {radius: settings.pointRadius,hoverRadius: 4,hitRadius: 6,pointStyle: "circle"}}, + scales: {r: {beginAtZero: true, + grid: {display: true}, + angleLines: {display: true}, + pointLabels: {display: true,font: {family: fontFamily,size: 11}}, + ticks: {display: true,backdropColor: "transparent",font: {family: fontFamily,size: 10} + } + } + } + }; default: return {}; @@ -563,7 +619,7 @@ export function getBaseDefaults(settings: PlotPluginSettings): ChartOptions { this.plugin.settings.backgroundColor = DEFAULT_SETTINGS.backgroundColor; await this.plugin.saveSettings(); - this.display; + this.display(); }) ) @@ -219,7 +219,7 @@ export class PlotSettingTab extends PluginSettingTab { .onClick(async () => { this.plugin.settings.saveImagesPath = DEFAULT_SETTINGS.saveImagesPath; await this.plugin.saveSettings(); - this.display; + this.display(); }) ); @@ -360,8 +360,8 @@ const plotSchema: PlotSettingSchema[] = [ { type: "toggle", key: "titleStatus", - name: "Don't display titles", - desc: "If on titles will not display unless you explicitly set them to true. Example: obj.plugins.title.display = true" + name: "Display titles", + desc: "Show titiles by default" }, { type: "number",