mirror of
https://github.com/mprojectscode/obsidian-bases-charts-plugin.git
synced 2026-07-22 06:43:19 +00:00
cleanup and clicking
This commit is contained in:
parent
6347f4caaf
commit
a086b18a5e
16 changed files with 259 additions and 219 deletions
1
exampleVault/Embed Test.md
Normal file
1
exampleVault/Embed Test.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
![[penguins.base]]
|
||||
|
|
@ -6,4 +6,4 @@ Low: 174.520004
|
|||
Close: 176.820007
|
||||
Adj Close: 176.140793
|
||||
Volume: 28401400
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ Low: 175.070007
|
|||
Close: 176.940002
|
||||
Adj Close: 176.26033
|
||||
Volume: 23774100
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ Low: 89.681427
|
|||
Close: 90.76857
|
||||
Adj Close: 84.457962
|
||||
Volume: 94118500
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ Low: 101.559998
|
|||
Close: 102.25
|
||||
Adj Close: 95.614388
|
||||
Volume: 68460000
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ culmen_depth_mm: 20.8
|
|||
flipper_length_mm: 201
|
||||
body_mass_g: 4300
|
||||
sex: MALE
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ culmen_depth_mm: 19.8
|
|||
flipper_length_mm: 207
|
||||
body_mass_g: 4000
|
||||
sex: MALE
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -6,4 +6,4 @@ culmen_depth_mm: 14.3
|
|||
flipper_length_mm: 218
|
||||
body_mass_g: 5700
|
||||
sex: MALE
|
||||
---
|
||||
---
|
||||
|
|
|
|||
|
|
@ -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<ChartId, GroupId> {
|
||||
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<number | Date | string, number>();
|
||||
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<string, BasesPropertyId> {
|
||||
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<BasesPropertyId, string> {
|
||||
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<void> {
|
||||
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',
|
||||
|
|
|
|||
|
|
@ -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 @@
|
|||
</script>
|
||||
|
||||
<PlotGrid view={view}>
|
||||
{#snippet chartSnippet({ data, chart, xName, group, height })}
|
||||
{#snippet chartSnippet({ data, chartIndex, xName, groupFn, height })}
|
||||
<Plot
|
||||
color={{ legend: true, scheme: OBSIDIAN_COLOR_PALETTE as unknown as string }}
|
||||
x={{ label: xName, type: 'band' }}
|
||||
y={{ label: `↑ ${data.getChartName(chart, view)}`, tickFormat: show_percentages ? d => `${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 @@
|
|||
<AxisY fill="var(--bases-charts-text)" stroke="var(--bases-charts-text)" opacity={1} />
|
||||
<GridY stroke="var(--bases-charts-grid)" strokeOpacity={1} />
|
||||
|
||||
<BarY data={data.getFlat(chart)} x="x" y="y" fill={group} />
|
||||
<BarY data={data.getFlat(chartIndex)} x="x" y="y" fill={groupFn} />
|
||||
{#if show_labels}
|
||||
<Text
|
||||
data={data.getStacked(chart)}
|
||||
data={data.getStacked(chartIndex)}
|
||||
x="x"
|
||||
y="y"
|
||||
fill="var(--bases-charts-text)"
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
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 @@
|
|||
</script>
|
||||
|
||||
<PlotGrid view={view}>
|
||||
{#snippet chartSnippet({ data, chart, xName, isGrouped, group, height })}
|
||||
{@const dataPoints = data.getFlat(chart, true)}
|
||||
<Plot
|
||||
grid
|
||||
color={{ legend: isGrouped, scheme: OBSIDIAN_COLOR_PALETTE as unknown as string }}
|
||||
x={{ label: xName }}
|
||||
y={{ label: `↑ ${data.getChartName(chart, view)}` }}
|
||||
height={height}
|
||||
class="bases-charts-plot"
|
||||
>
|
||||
{#snippet chartSnippet({ data, chartIndex, xName, groupFn, height, setHoveredData })}
|
||||
{@const dataPoints = data.getFlat(chartIndex, true)}
|
||||
<Plot grid x={{ label: xName }} y={{ label: `↑ ${data.getChartName(chartIndex)}` }} height={height} class="bases-charts-plot">
|
||||
<AxisX fill="var(--bases-charts-text)" stroke="var(--bases-charts-text)" opacity={1} />
|
||||
<AxisY fill="var(--bases-charts-text)" stroke="var(--bases-charts-text)" opacity={1} />
|
||||
<GridX stroke="var(--bases-charts-grid)" strokeOpacity={1} />
|
||||
<GridY stroke="var(--bases-charts-grid)" strokeOpacity={1} />
|
||||
|
||||
<Line data={dataPoints} x="x" y="y" stroke="group" />
|
||||
<Pointer data={dataPoints} x="x" z={group} maxDistance={50}>
|
||||
<Line data={dataPoints} x="x" y="y" stroke={groupFn} />
|
||||
<Pointer data={dataPoints} x="x" z={groupFn} maxDistance={50} onupdate={setHoveredData}>
|
||||
{#snippet children({ data })}
|
||||
<RuleX data={data} x="x" stroke="var(--bases-charts-muted)" />
|
||||
<RuleX data={data} x="x" stroke="var(--bases-charts-grid-hover)" />
|
||||
<Text data={data} x="x" y="y" fill="var(--bases-charts-text)" text={d => toCompactString(d.y)} lineAnchor="bottom" dy={-10} />
|
||||
<Dot data={data} x="x" y="y" stroke="var(--bases-charts-text)" />
|
||||
{/snippet}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
<script lang="ts">
|
||||
import { onMount, type Snippet } from 'svelte';
|
||||
import { CHART_SETTINGS, type ChartView, type DataWrapper } from '../ChartView';
|
||||
import { type FullChartProps } from '../utils';
|
||||
import { AbstractDataWrapper, CHART_SETTINGS, type ChartView, type DataWrapper } from '../ChartView';
|
||||
import { OBSIDIAN_DEFAULT_SINGLE_COLOR, type FullChartProps } from '../utils/utils';
|
||||
import PlotGridItem from './PlotGridItem.svelte';
|
||||
|
||||
interface Props {
|
||||
view: ChartView;
|
||||
chartSnippet: Snippet<[FullChartProps]>;
|
||||
chartSnippet: Snippet<[FullChartProps<string, string>]>;
|
||||
}
|
||||
|
||||
let { view, chartSnippet }: Props = $props();
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
let data: DataWrapper | null = $state(null) as DataWrapper | null;
|
||||
let xName: string = $state('');
|
||||
let isGrouped: boolean = $derived(data?.hasMultipleGroups() ?? false);
|
||||
let group: string = $derived(data?.getChartGroup() ?? 'var(--bases-charts-accent)');
|
||||
let groupFn = $derived(data?.getChartGroupIdentifier() ?? OBSIDIAN_DEFAULT_SINGLE_COLOR);
|
||||
|
||||
function onUpdate() {
|
||||
const xField = view.config?.getAsPropertyId(CHART_SETTINGS.X);
|
||||
|
|
@ -33,17 +33,29 @@
|
|||
});
|
||||
</script>
|
||||
|
||||
<div class="bases-charts-plot-legend">
|
||||
{#if data}
|
||||
{#each data?.getGroupIdentifiers() ?? [] as _, groupIndex}
|
||||
<div class="bases-charts-plot-legend-item">
|
||||
<div class="bases-charts-plot-legend-color" style="--color: {data.getColorFromGroupIndex(groupIndex)}"></div>
|
||||
<span class="bases-charts-plot-legend-label">{data.getGroupName(groupIndex)}</span>
|
||||
</div>
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="bases-charts-plot-grid">
|
||||
{#if data}
|
||||
{#each data.getCharts() as chart}
|
||||
{#each data.getChartIdentifiers() as _, chartIndex}
|
||||
<PlotGridItem
|
||||
view={view}
|
||||
chartSnippet={chartSnippet}
|
||||
chartProps={{
|
||||
data,
|
||||
chart,
|
||||
chartIndex,
|
||||
xName,
|
||||
isGrouped,
|
||||
group,
|
||||
groupFn,
|
||||
}}
|
||||
></PlotGridItem>
|
||||
{:else}
|
||||
|
|
@ -57,10 +69,32 @@
|
|||
<style>
|
||||
.bases-charts-plot-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
grid-template-columns: repeat(auto-fit, minmax(var(--bases-charts-min-width), 1fr));
|
||||
gap: var(--size-4-4);
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--size-4-4);
|
||||
}
|
||||
|
||||
.bases-charts-plot-legend {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--size-4-3);
|
||||
padding-inline: var(--size-4-4);
|
||||
padding-top: var(--size-4-4);
|
||||
}
|
||||
|
||||
.bases-charts-plot-legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--size-2-2);
|
||||
font-size: var(--font-small);
|
||||
color: var(--bases-charts-text);
|
||||
}
|
||||
|
||||
.bases-charts-plot-legend-color {
|
||||
width: var(--size-4-4);
|
||||
height: var(--size-4-4);
|
||||
background-color: var(--color);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,23 +1,44 @@
|
|||
<script lang="ts">
|
||||
import type { Snippet } from 'svelte';
|
||||
import type { ChartProps, FullChartProps } from '../utils';
|
||||
import type { ChartProps, FullChartProps } from '../utils/utils';
|
||||
import type { ChartView, ProcessedData } from '../ChartView';
|
||||
|
||||
interface Props {
|
||||
chartProps: ChartProps;
|
||||
chartSnippet: Snippet<[FullChartProps]>;
|
||||
view: ChartView;
|
||||
chartProps: ChartProps<string, string>;
|
||||
chartSnippet: Snippet<[FullChartProps<string, string>]>;
|
||||
}
|
||||
|
||||
let { chartProps, chartSnippet }: Props = $props();
|
||||
let { view, chartProps, chartSnippet }: Props = $props();
|
||||
|
||||
let width = $state(0);
|
||||
let height = $state(0);
|
||||
|
||||
let hoveredData: ProcessedData[] = $state([]);
|
||||
|
||||
function setHoveredData(data: ProcessedData[]) {
|
||||
hoveredData = data;
|
||||
}
|
||||
|
||||
function onClick(e: MouseEvent) {
|
||||
if (hoveredData.length < 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newTab = e.ctrlKey || e.metaKey;
|
||||
const filePath = hoveredData[0].file;
|
||||
view.openFile(filePath, newTab);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="bases-charts-plot-grid-item" bind:clientWidth={width} bind:clientHeight={height}>
|
||||
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<div class="bases-charts-plot-grid-item" bind:clientWidth={width} bind:clientHeight={height} onclick={onClick}>
|
||||
{@render chartSnippet({
|
||||
...chartProps,
|
||||
width,
|
||||
height,
|
||||
setHoveredData,
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
|
@ -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);
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
<script lang="ts">
|
||||
import { AxisX, AxisY, Dot, GridX, GridY, Plot, Pointer, Text } from 'svelteplot';
|
||||
import { ChartView } from '../ChartView';
|
||||
import { OBSIDIAN_COLOR_PALETTE, toCompactString } from '../utils';
|
||||
import { toCompactString } from '../utils/utils';
|
||||
import PlotGrid from './PlotGrid.svelte';
|
||||
|
||||
interface Props {
|
||||
|
|
@ -12,26 +12,19 @@
|
|||
</script>
|
||||
|
||||
<PlotGrid view={view}>
|
||||
{#snippet chartSnippet({ data, chart, xName, isGrouped, group, height })}
|
||||
{@const dataPoints = data.getFlat(chart)}
|
||||
<Plot
|
||||
grid
|
||||
color={{ legend: isGrouped, scheme: OBSIDIAN_COLOR_PALETTE as unknown as string }}
|
||||
x={{ label: xName }}
|
||||
y={{ label: `↑ ${data.getChartName(chart, view)}` }}
|
||||
height={height}
|
||||
class="bases-chart"
|
||||
>
|
||||
{#snippet chartSnippet({ data, chartIndex, xName, groupFn, height, setHoveredData })}
|
||||
{@const dataPoints = data.getFlat(chartIndex)}
|
||||
<Plot grid x={{ label: xName }} y={{ label: `↑ ${data.getChartName(chartIndex)}` }} height={height} class="bases-charts-plot">
|
||||
<AxisX fill="var(--bases-charts-text)" stroke="var(--bases-charts-text)" opacity={1} />
|
||||
<AxisY fill="var(--bases-charts-text)" stroke="var(--bases-charts-text)" opacity={1} />
|
||||
<GridX stroke="var(--bases-charts-grid)" strokeOpacity={1} />
|
||||
<GridY stroke="var(--bases-charts-grid)" strokeOpacity={1} />
|
||||
|
||||
<Dot data={dataPoints} x="x" y="y" stroke={group} />
|
||||
<Pointer data={dataPoints} x="x" y="y" maxDistance={50}>
|
||||
<Dot data={dataPoints} x="x" y="y" stroke={groupFn} />
|
||||
<Pointer data={dataPoints} x="x" y="y" maxDistance={50} onupdate={setHoveredData}>
|
||||
{#snippet children({ data })}
|
||||
<Text data={data} x="x" y="y" fill="var(--bases-charts-text)" text={d => toCompactString(d.y)} lineAnchor="bottom" dy={-10} />
|
||||
<Dot data={data} x="x" y="y" fill={group} stroke="var(--bases-charts-text)" />
|
||||
<Dot data={data} x="x" y="y" fill={groupFn} stroke="var(--bases-charts-text)" />
|
||||
{/snippet}
|
||||
</Pointer>
|
||||
</Plot>
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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<ChartId, GroupId> {
|
||||
data: AbstractDataWrapper<ChartId, GroupId>;
|
||||
chartIndex: number;
|
||||
xName: string;
|
||||
isGrouped: boolean;
|
||||
group: string;
|
||||
groupFn: (d: ProcessedData) => string;
|
||||
}
|
||||
|
||||
export type FullChartProps = ChartProps & {
|
||||
export type FullChartProps<ChartId, GroupId> = ChartProps<ChartId, GroupId> & {
|
||||
width: number;
|
||||
height: number;
|
||||
setHoveredData: (data: ProcessedData[]) => void;
|
||||
};
|
||||
Loading…
Reference in a new issue