Added radar chart type

This commit is contained in:
Jomar 2026-04-16 09:54:01 -07:00
parent 2672e5215b
commit cd2c12174f
5 changed files with 371 additions and 87 deletions

View file

@ -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);

View file

@ -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;

View file

@ -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<string,string> = {
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"
}

View file

@ -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<ChartType> {
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<Char
zoom: {wheel: {enabled: settings.zoomStatus ? false : true},
pinch: {enabled: settings.zoomStatus ? false : true}, mode: "xy"}
},
title: {display: !settings.titleStatus, font: {family: fontFamily, size: 13, weight: 600}},
title: {display: !settings.titleStatus, text: "", font: {family: fontFamily, size: 13, weight: 600}},
tooltip: {enabled: true, padding: 10, cornerRadius: 8}
}
};

View file

@ -39,7 +39,7 @@ export const DEFAULT_SETTINGS: PlotPluginSettings = {
showBorder: true,
transparentBackground: true,
backgroundColor: "var(--background-secondary)",
titleStatus: false,
titleStatus: true,
zoomStatus: true,
saveImagesPath: "Attachments/Charts/"
}
@ -166,7 +166,7 @@ export class PlotSettingTab extends PluginSettingTab {
.onClick(async () => {
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",