From a086b18a5edcf8af8d358d622de01428be89fdb9 Mon Sep 17 00:00:00 2001 From: Moritz Jung Date: Tue, 7 Oct 2025 13:02:06 +0200 Subject: [PATCH] cleanup and clicking --- exampleVault/Embed Test.md | 1 + exampleVault/aapl/1211.md | 2 +- exampleVault/aapl/1214.md | 2 +- exampleVault/aapl/263.md | 2 +- exampleVault/aapl/327.md | 2 +- exampleVault/penguins/182.md | 2 +- exampleVault/penguins/214.md | 2 +- exampleVault/penguins/239.md | 2 +- packages/obsidian/src/ChartView.ts | 305 +++++++++--------- packages/obsidian/src/charts/BarPlot.svelte | 11 +- packages/obsidian/src/charts/LinePlot.svelte | 21 +- packages/obsidian/src/charts/PlotGrid.svelte | 50 ++- .../obsidian/src/charts/PlotGridItem.svelte | 34 +- .../obsidian/src/charts/ScatterPlot.svelte | 21 +- packages/obsidian/src/styles.css | 6 +- packages/obsidian/src/{ => utils}/utils.ts | 15 +- 16 files changed, 259 insertions(+), 219 deletions(-) create mode 100644 exampleVault/Embed Test.md rename packages/obsidian/src/{ => utils}/utils.ts (63%) diff --git a/exampleVault/Embed Test.md b/exampleVault/Embed Test.md new file mode 100644 index 0000000..a9781c9 --- /dev/null +++ b/exampleVault/Embed Test.md @@ -0,0 +1 @@ +![[penguins.base]] \ No newline at end of file diff --git a/exampleVault/aapl/1211.md b/exampleVault/aapl/1211.md index 035668f..91797c0 100644 --- a/exampleVault/aapl/1211.md +++ b/exampleVault/aapl/1211.md @@ -6,4 +6,4 @@ Low: 174.520004 Close: 176.820007 Adj Close: 176.140793 Volume: 28401400 ---- \ No newline at end of file +--- diff --git a/exampleVault/aapl/1214.md b/exampleVault/aapl/1214.md index 63bce5f..bc497ca 100644 --- a/exampleVault/aapl/1214.md +++ b/exampleVault/aapl/1214.md @@ -6,4 +6,4 @@ Low: 175.070007 Close: 176.940002 Adj Close: 176.26033 Volume: 23774100 ---- \ No newline at end of file +--- diff --git a/exampleVault/aapl/263.md b/exampleVault/aapl/263.md index bc5fdc0..5645ca5 100644 --- a/exampleVault/aapl/263.md +++ b/exampleVault/aapl/263.md @@ -6,4 +6,4 @@ Low: 89.681427 Close: 90.76857 Adj Close: 84.457962 Volume: 94118500 ---- \ No newline at end of file +--- diff --git a/exampleVault/aapl/327.md b/exampleVault/aapl/327.md index 841b764..2665305 100644 --- a/exampleVault/aapl/327.md +++ b/exampleVault/aapl/327.md @@ -6,4 +6,4 @@ Low: 101.559998 Close: 102.25 Adj Close: 95.614388 Volume: 68460000 ---- \ No newline at end of file +--- diff --git a/exampleVault/penguins/182.md b/exampleVault/penguins/182.md index e53482c..405e266 100644 --- a/exampleVault/penguins/182.md +++ b/exampleVault/penguins/182.md @@ -6,4 +6,4 @@ culmen_depth_mm: 20.8 flipper_length_mm: 201 body_mass_g: 4300 sex: MALE ---- \ No newline at end of file +--- diff --git a/exampleVault/penguins/214.md b/exampleVault/penguins/214.md index 9216b47..dfe01e1 100644 --- a/exampleVault/penguins/214.md +++ b/exampleVault/penguins/214.md @@ -6,4 +6,4 @@ culmen_depth_mm: 19.8 flipper_length_mm: 207 body_mass_g: 4000 sex: MALE ---- \ No newline at end of file +--- diff --git a/exampleVault/penguins/239.md b/exampleVault/penguins/239.md index 952e3c9..8edd2af 100644 --- a/exampleVault/penguins/239.md +++ b/exampleVault/penguins/239.md @@ -6,4 +6,4 @@ culmen_depth_mm: 14.3 flipper_length_mm: 218 body_mass_g: 5700 sex: MALE ---- \ No newline at end of file +--- diff --git a/packages/obsidian/src/ChartView.ts b/packages/obsidian/src/ChartView.ts index 628a566..1778d6c 100644 --- a/packages/obsidian/src/ChartView.ts +++ b/packages/obsidian/src/ChartView.ts @@ -1,9 +1,10 @@ -import type { BasesEntry, BasesQueryResult, QueryController, Value } from 'obsidian'; +import type { BasesEntry, QueryController, Value } from 'obsidian'; import type { BasesPropertyId, ViewOption } from 'obsidian'; import { BasesView, DateValue, Events, NumberValue, StringValue } from 'obsidian'; import BarPlot from 'packages/obsidian/src/charts/BarPlot.svelte'; import LinePlot from 'packages/obsidian/src/charts/LinePlot.svelte'; import ScatterPlot from 'packages/obsidian/src/charts/ScatterPlot.svelte'; +import { OBSIDIAN_COLOR_PALETTE, OBSIDIAN_DEFAULT_SINGLE_COLOR } from 'packages/obsidian/src/utils/utils'; import { mount, unmount } from 'svelte'; export const SCATTER_CHART_VIEW_TYPE = 'chart-scatter'; @@ -14,7 +15,6 @@ export type ChartViewType = typeof SCATTER_CHART_VIEW_TYPE | typeof LINE_CHART_V export const CHART_SETTINGS = { X: 'x', - LABEL: 'label', SHOW_PERCENTAGES: 'show-percentages', SHOW_LABELS: 'show-labels', MULTI_CHART: 'multi-chart-mode', @@ -24,9 +24,19 @@ export const CHART_SETTINGS = { export type ProcessedData = { x: number | Date | string; y: number; - yProperty: BasesPropertyId; - label?: string; - group?: string; + /** + * This is always the value by which we color the data point. + * In group separated mode, this is the display name of the property. + * In property separated mode, this is the group by value. + */ + groupIndex: number; + /** + * This is always the chart to sort the data point into. + * In group separated mode, this is the group by value. + * In property separated mode, this is the property id. + */ + chartIndex: number; + file: string; }; export interface ProcessedGroupEntry { @@ -49,120 +59,130 @@ export enum MultiChartMode { PROPERTY = 'Separate by property', } -export class DataWrapper { - readonly data: ProcessedGroupedData; - readonly properties: BasesPropertyId[]; - readonly mode: MultiChartMode; +function sortDataByGroup(data: ProcessedData[]): ProcessedData[] { + return data.sort((a, b) => { + if (a.groupIndex != null && b.groupIndex != null) { + return a.groupIndex - b.groupIndex; + } + return 0; + }); +} - constructor(data: ProcessedGroupedData, properties: BasesPropertyId[], mode: MultiChartMode) { +export abstract class AbstractDataWrapper { + readonly view: ChartView; + readonly data: ProcessedData[]; + readonly groupBySet: string[]; + + constructor(view: ChartView, data: ProcessedData[], groupBySet: string[]) { + this.view = view; this.data = data; - this.properties = properties; - this.mode = mode; - - if (this.data.grouped) { - this.data.groups.sort((a, b) => a.key.localeCompare(b.key)); - } + this.groupBySet = groupBySet; } - static empty(): DataWrapper { - return new DataWrapper({ grouped: false, entries: [] }, [], MultiChartMode.PROPERTY); - } + abstract getChartIdentifiers(): ChartId[]; + abstract getGroupIdentifiers(): GroupId[]; - getCharts(): string[] { - if (this.mode === MultiChartMode.GROUP) { - if (this.data.grouped) { - return this.data.groups.map(g => g.key); - } else { - return ['All']; - } - } else { - return this.properties; - } - } + abstract getChartName(chartIndex: number): string; + abstract getGroupName(groupIndex: number): string; - getChartName(chart: string, view: ChartView): string { - if (this.mode === MultiChartMode.GROUP) { - return chart; - } else { - const xProp = view.config.getAsPropertyId(CHART_SETTINGS.X); - return xProp ? view.config.getDisplayName(xProp) : 'Unknown'; - } - } - - getChartGroup(): string { - if (this.mode === MultiChartMode.GROUP && this.properties.length > 1) { - return 'group'; - } else if (this.mode === MultiChartMode.PROPERTY && this.data.grouped) { - return 'group'; + getChartGroupIdentifier(): (d: ProcessedData) => string { + if (this.hasMultipleGroups()) { + return d => this.getColorFromGroupIndex(d.groupIndex); } - return 'var(--bases-charts-accent)'; + return OBSIDIAN_DEFAULT_SINGLE_COLOR; + } + + getColorFromGroupIndex(groupIndex: number): string { + return OBSIDIAN_COLOR_PALETTE[groupIndex % OBSIDIAN_COLOR_PALETTE.length]; } hasMultipleGroups(): boolean { - return (this.mode === MultiChartMode.GROUP && this.properties.length > 1) || (this.mode === MultiChartMode.PROPERTY && this.data.grouped); + return this.getGroupIdentifiers().length > 1; } - getFlat(chart: string, sorted: boolean = false): ProcessedData[] { - let data: ProcessedData[]; + hasMultipleCharts(): boolean { + return this.getChartIdentifiers().length > 1; + } - if (this.mode === MultiChartMode.GROUP) { - if (this.data.grouped) { - const group = this.data.groups.find(g => g.key === chart); - data = group ? group.entries : []; - } else { - data = this.data.entries; - } - } else { - if (this.data.grouped) { - data = this.data.groups.flatMap(g => g.entries.filter(d => d.yProperty === chart)); - } else { - data = this.data.entries.filter(d => d.yProperty === chart); - } - } + getFlat(chartIndex: number, sorted: boolean = false): ProcessedData[] { + const data = this.data.filter(d => d.chartIndex === chartIndex); if (sorted) { - data = data.sort((a, b) => { - if (a.group != null && b.group != null) { - return a.group.localeCompare(b.group); - } - return 0; - }); + return sortDataByGroup(data); } return data; } - getStacked(chart: string): ProcessedData[] { - if (this.mode === MultiChartMode.GROUP) { - throw new Error('Stacked data is only available in property mode'); - } - - if (!this.data.grouped) { - return this.data.entries; - } - + getStacked(chartIndex: number): ProcessedData[] { const xMap = new Map(); const stackedData: ProcessedData[] = []; - for (const group of this.data.groups) { - for (const entry of group.entries.filter(d => d.yProperty === chart)) { - const prevY = xMap.get(entry.x) ?? 0; - const newY = prevY + entry.y; - - stackedData.push({ - ...entry, - y: newY, - }); - - xMap.set(entry.x, newY); + for (const entry of this.data) { + if (entry.chartIndex !== chartIndex) { + continue; } + + const prevY = xMap.get(entry.x) ?? 0; + const newY = prevY + entry.y; + + stackedData.push({ + ...entry, + y: newY, + }); + + xMap.set(entry.x, newY); } return stackedData; } } +export class GroupSeparatedData extends AbstractDataWrapper { + getChartIdentifiers(): string[] { + return this.groupBySet; + } + + getGroupIdentifiers(): BasesPropertyId[] { + return this.view.data.properties; + } + + getChartName(chartIndex: number): string { + return this.getChartIdentifiers()[chartIndex] ?? `Chart ${chartIndex + 1}`; + } + + getGroupName(groupIndex: number): string { + const groupId = this.getGroupIdentifiers()[groupIndex]; + return this.view.config.getDisplayName(groupId) ?? `Group ${groupIndex + 1}`; + } +} + +export class PropertySeparatedData extends AbstractDataWrapper { + getChartIdentifiers(): BasesPropertyId[] { + return this.view.data.properties; + } + + getGroupIdentifiers(): string[] { + return this.groupBySet; + } + + getChartName(chartIndex: number): string { + const chartId = this.getChartIdentifiers()[chartIndex]; + return this.view.config.getDisplayName(chartId) ?? `Chart ${chartIndex + 1}`; + } + + getGroupName(groupIndex: number): string { + return this.getGroupIdentifiers()[groupIndex] ?? `Group ${groupIndex + 1}`; + } +} + +export type DataWrapper = GroupSeparatedData | PropertySeparatedData; + +export function emptyDataWrapper(view: ChartView): DataWrapper { + return new GroupSeparatedData(view, [], []); +} + export function parseValueAsNumber(value: Value | null): number | null { if (!value) { return null; @@ -245,54 +265,42 @@ export class ChartView extends BasesView { } processData(): DataWrapper { - const queryResult: BasesQueryResult | null = this.data; const xField = this.config.getAsPropertyId(CHART_SETTINGS.X); - const labelField = this.config.getAsPropertyId(CHART_SETTINGS.LABEL); const mode = this.config.get(CHART_SETTINGS.MULTI_CHART) ?? MultiChartMode.PROPERTY; if (mode !== MultiChartMode.GROUP && mode !== MultiChartMode.PROPERTY) { // eslint-disable-next-line @typescript-eslint/no-base-to-string console.warn(`Invalid multi chart mode: ${mode}`); - return DataWrapper.empty(); + return emptyDataWrapper(this); } if (!xField) { - return DataWrapper.empty(); + return emptyDataWrapper(this); } - if (this.isGrouped()) { - const data: ProcessedGroupedData = { - grouped: true, - groups: [], - }; + const data: ProcessedData[] = []; + const groupBySet = this.data.groupedData.map(g => g.key?.toString()).filter(v => v != null); + groupBySet.sort(); - for (const group of queryResult?.groupedData ?? []) { - const groupData: ProcessedGroupEntry = { - key: group.key!.toString(), - entries: [], - }; - - for (const entry of group.entries) { - const processedEntry = this.processEntry(entry, xField, labelField, group.key?.toString(), mode); - groupData.entries.push(...processedEntry); - } - - data.groups.push(groupData); + for (const group of this.data?.groupedData ?? []) { + const groupKey = group.key?.toString(); + let groupIndex: number; + if (groupKey == null) { + groupIndex = 0; + } else { + groupIndex = groupBySet.indexOf(groupKey); } - return new DataWrapper(data, this.data.properties, mode); + for (const entry of group.entries) { + const processedEntry = this.processEntry(entry, xField, groupIndex, mode); + data.push(...processedEntry); + } + } + + if (mode === MultiChartMode.GROUP) { + return new GroupSeparatedData(this, data, groupBySet); } else { - const data: ProcessedGroupedData = { - grouped: false, - entries: [], - }; - - for (const entry of queryResult?.data ?? []) { - const processedEntry = this.processEntry(entry, xField, labelField, undefined, mode); - data.entries.push(...processedEntry); - } - - return new DataWrapper(data, this.data.properties, mode); + return new PropertySeparatedData(this, data, groupBySet); } } @@ -300,35 +308,27 @@ export class ChartView extends BasesView { return !(this.data.groupedData?.length === 1 && this.data.groupedData[0].key == null); } - processEntry( - entry: BasesEntry, - xField: BasesPropertyId, - labelField: BasesPropertyId | null, - group: string | undefined, - mode: MultiChartMode, - ): ProcessedData[] { + processEntry(entry: BasesEntry, xField: BasesPropertyId, groupIndex: number, mode: MultiChartMode): ProcessedData[] { try { const x = entry.getValue(xField); - const label = labelField ? entry.getValue(labelField) : null; const xValue = parseValueAsX(x); - const labelStr = label?.toString(); if (xValue === null) { return []; } const result: ProcessedData[] = []; - for (const prop of this.data.properties) { + for (let i = 0; i < this.data.properties.length; i++) { + const prop = this.data.properties[i]; const yValue = parseValueAsNumber(entry.getValue(prop)); - const yName = this.config.getDisplayName(prop); if (xValue !== null && yValue !== null) { result.push({ x: xValue, y: yValue, - yProperty: prop, - label: labelStr, - group: mode === MultiChartMode.GROUP ? yName : group, + groupIndex: mode === MultiChartMode.GROUP ? i : groupIndex, + chartIndex: mode === MultiChartMode.GROUP ? groupIndex : i, + file: entry.file.path, }); } } @@ -341,6 +341,20 @@ export class ChartView extends BasesView { return []; } + async openFile(filePath: string, newTab: boolean): Promise { + const tFile = this.app.vault.getFileByPath(filePath); + if (!tFile) { + return; + } + + const activeLeaf = this.app.workspace.getLeaf(newTab ? 'tab' : false); + if (activeLeaf) { + await activeLeaf.openFile(tFile, { + state: { mode: 'source' }, + }); + } + } + static getViewOptions(type: ChartViewType): ViewOption[] { if (type === SCATTER_CHART_VIEW_TYPE) { return ChartView.scatterViewOptions(); @@ -372,13 +386,6 @@ export class ChartView extends BasesView { filter: prop => !prop.startsWith('file.'), placeholder: 'Property', }, - { - displayName: 'Label', - type: 'property', - key: CHART_SETTINGS.LABEL, - filter: prop => !prop.startsWith('file.'), - placeholder: 'Property', - }, ]; } @@ -401,13 +408,6 @@ export class ChartView extends BasesView { filter: prop => !prop.startsWith('file.'), placeholder: 'Property', }, - { - displayName: 'Label', - type: 'property', - key: CHART_SETTINGS.LABEL, - filter: prop => !prop.startsWith('file.'), - placeholder: 'Property', - }, ]; } @@ -420,13 +420,6 @@ export class ChartView extends BasesView { filter: prop => !prop.startsWith('file.'), placeholder: 'Property', }, - { - displayName: 'Label', - type: 'property', - key: CHART_SETTINGS.LABEL, - filter: prop => !prop.startsWith('file.'), - placeholder: 'Property', - }, { displayName: 'Show labels', type: 'toggle', diff --git a/packages/obsidian/src/charts/BarPlot.svelte b/packages/obsidian/src/charts/BarPlot.svelte index 73498e9..2aaeb85 100644 --- a/packages/obsidian/src/charts/BarPlot.svelte +++ b/packages/obsidian/src/charts/BarPlot.svelte @@ -2,7 +2,7 @@ import { AxisX, AxisY, BarY, GridY, Plot, Text } from 'svelteplot'; import { CHART_SETTINGS, ChartView } from '../ChartView'; import { onMount } from 'svelte'; - import { OBSIDIAN_COLOR_PALETTE, toCompactString } from '../utils'; + import { OBSIDIAN_COLOR_PALETTE, toCompactString } from '../utils/utils'; import PlotGrid from './PlotGrid.svelte'; interface Props { @@ -29,11 +29,10 @@ - {#snippet chartSnippet({ data, chart, xName, group, height })} + {#snippet chartSnippet({ data, chartIndex, xName, groupFn, height })} `${String(d)}%` : d => toCompactString(d) }} + y={{ label: `↑ ${data.getChartName(chartIndex)}`, tickFormat: show_percentages ? d => `${String(d)}%` : d => toCompactString(d) }} height={height} class="bases-charts-plot" > @@ -41,10 +40,10 @@ - + {#if show_labels} import { Line, Plot, GridX, GridY, AxisY, AxisX, Pointer, Text, Dot, RuleX } from 'svelteplot'; import { ChartView } from '../ChartView'; - import { OBSIDIAN_COLOR_PALETTE, toCompactString } from '../utils'; + import { OBSIDIAN_COLOR_PALETTE, toCompactString } from '../utils/utils'; import PlotGrid from './PlotGrid.svelte'; interface Props { @@ -12,25 +12,18 @@ - {#snippet chartSnippet({ data, chart, xName, isGrouped, group, height })} - {@const dataPoints = data.getFlat(chart, true)} - + {#snippet chartSnippet({ data, chartIndex, xName, groupFn, height, setHoveredData })} + {@const dataPoints = data.getFlat(chartIndex, true)} + - - + + {#snippet children({ data })} - + toCompactString(d.y)} lineAnchor="bottom" dy={-10} /> {/snippet} diff --git a/packages/obsidian/src/charts/PlotGrid.svelte b/packages/obsidian/src/charts/PlotGrid.svelte index d51b958..0e0a00d 100644 --- a/packages/obsidian/src/charts/PlotGrid.svelte +++ b/packages/obsidian/src/charts/PlotGrid.svelte @@ -1,12 +1,12 @@ +
+ {#if data} + {#each data?.getGroupIdentifiers() ?? [] as _, groupIndex} +
+
+ {data.getGroupName(groupIndex)} +
+ {/each} + {/if} +
+
{#if data} - {#each data.getCharts() as chart} + {#each data.getChartIdentifiers() as _, chartIndex} {:else} @@ -57,10 +69,32 @@ diff --git a/packages/obsidian/src/charts/PlotGridItem.svelte b/packages/obsidian/src/charts/PlotGridItem.svelte index 44b96bd..1f51bff 100644 --- a/packages/obsidian/src/charts/PlotGridItem.svelte +++ b/packages/obsidian/src/charts/PlotGridItem.svelte @@ -1,23 +1,44 @@ -
+ + +
{@render chartSnippet({ ...chartProps, width, height, + setHoveredData, })}
@@ -26,6 +47,7 @@ position: relative; width: 100%; height: 100%; - min-height: 300px; + min-height: var(--bases-charts-min-height); + min-width: var(--bases-charts-min-width); } diff --git a/packages/obsidian/src/charts/ScatterPlot.svelte b/packages/obsidian/src/charts/ScatterPlot.svelte index 374c872..5c5d3ee 100644 --- a/packages/obsidian/src/charts/ScatterPlot.svelte +++ b/packages/obsidian/src/charts/ScatterPlot.svelte @@ -1,7 +1,7 @@ - {#snippet chartSnippet({ data, chart, xName, isGrouped, group, height })} - {@const dataPoints = data.getFlat(chart)} - + {#snippet chartSnippet({ data, chartIndex, xName, groupFn, height, setHoveredData })} + {@const dataPoints = data.getFlat(chartIndex)} + - - + + {#snippet children({ data })} toCompactString(d.y)} lineAnchor="bottom" dy={-10} /> - + {/snippet} diff --git a/packages/obsidian/src/styles.css b/packages/obsidian/src/styles.css index d8a7508..5c59550 100644 --- a/packages/obsidian/src/styles.css +++ b/packages/obsidian/src/styles.css @@ -1,8 +1,10 @@ body { --bases-charts-text: var(--text-normal); - --bases-charts-muted: var(--color-base-60); - --bases-charts-grid: var(--color-base-30); + --bases-charts-grid: var(--background-modifier-border); + --bases-charts-grid-hover: var(--background-modifier-border); --bases-charts-accent: var(--color-accent); + --bases-charts-min-width: 350px; + --bases-charts-min-height: 350px; } .bases-charts-plot { diff --git a/packages/obsidian/src/utils.ts b/packages/obsidian/src/utils/utils.ts similarity index 63% rename from packages/obsidian/src/utils.ts rename to packages/obsidian/src/utils/utils.ts index 46ce957..244c0fd 100644 --- a/packages/obsidian/src/utils.ts +++ b/packages/obsidian/src/utils/utils.ts @@ -1,4 +1,4 @@ -import type { DataWrapper } from 'packages/obsidian/src/ChartView'; +import type { AbstractDataWrapper, ProcessedData } from 'packages/obsidian/src/ChartView'; export function toCompactString(datum: number | string | symbol | boolean | Date | null | undefined): string { if (datum == null) { @@ -30,15 +30,18 @@ export const OBSIDIAN_COLOR_PALETTE = [ 'var(--color-pink)', ]; -export interface ChartProps { - data: DataWrapper; - chart: string; +export const OBSIDIAN_DEFAULT_SINGLE_COLOR = (_: unknown): string => 'var(--bases-charts-accent)'; + +export interface ChartProps { + data: AbstractDataWrapper; + chartIndex: number; xName: string; isGrouped: boolean; - group: string; + groupFn: (d: ProcessedData) => string; } -export type FullChartProps = ChartProps & { +export type FullChartProps = ChartProps & { width: number; height: number; + setHoveredData: (data: ProcessedData[]) => void; };