Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions blocksuite/affine/blocks/data-view/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { viewPresets } from '@blocksuite/data-view/view-presets';
export const blockQueryViews: ViewMeta[] = [
viewPresets.tableViewMeta,
viewPresets.kanbanViewMeta,
viewPresets.chartViewMeta,
];

export const blockQueryViewMap = Object.fromEntries(
Expand Down
39 changes: 38 additions & 1 deletion blocksuite/affine/blocks/database/src/configs/slash-menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '@blocksuite/icons/lit';

import { insertDatabaseBlockCommand } from '../commands';
import { KanbanViewTooltip, TableViewTooltip } from './tooltips';
import { KanbanViewTooltip, TableViewTooltip, ChartViewTooltip } from './tooltips';

export const databaseSlashMenuConfig: SlashMenuConfig = {
disableWhen: ({ model }) => model.flavour === 'affine:database',
Expand Down Expand Up @@ -79,5 +79,42 @@ export const databaseSlashMenuConfig: SlashMenuConfig = {
.run();
},
},

// ─────────────────────────────────────────────────────────────────────────
// ► NEW “Chart View” ENTRY
// ─────────────────────────────────────────────────────────────────────────
{
name: 'Chart View',
description: 'Display items as a chart.',
searchAlias: ['chart'],
icon: DatabaseKanbanViewIcon(),
tooltip: {
figure: ChartViewTooltip,
caption: 'Chart View',
},
group: '7_Database@4',
when: ({ model }) =>
!isInsideBlockByFlavour(model.store, model, 'affine:edgeless-text'),
action: ({ std }) => {
std.command
.chain()
.pipe(getSelectedModelsCommand)
.pipe(insertDatabaseBlockCommand, {
viewType: viewPresets.chartViewMeta.type,
place: 'after',
removeEmptyLine: true,
})
.pipe(({ insertedDatabaseBlockId }) => {
if (insertedDatabaseBlockId) {
const telemetry = std.getOptional(TelemetryProvider);
telemetry?.track('BlockCreated', {
blockType: 'affine:database',
});
}
})
.run();
},
},
// ─────────────────────────────────────────────────────────────────────────
],
};
23 changes: 23 additions & 0 deletions blocksuite/affine/blocks/database/src/configs/tooltips.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,3 +179,26 @@ export const ToDoListTooltip = html`<svg width="170" height="68" viewBox="0 0 17
</g>
</svg>
`;


// prettier-ignore
export const ChartViewTooltip = html`
<svg
aria-hidden="true"
role="graphics-symbol"
viewBox="0 0 20 20"
class="viewChart"
style="
width: 20px;
height: 20px;
display: block;
fill: rgba(255, 255, 255, 0.81);
flex-shrink: 0;
margin-right: 6px;
"
>
<path
d="M2.375 10a7.625 7.625 0 1 1 15.25 0 7.625 7.625 0 0 1-15.25 0m1.25 0a6.375 6.375 0 0 0 11.208 4.157L9.65 10.632a.63.63 0 0 1-.274-.517v-6.46A6.376 6.376 0 0 0 3.625 10m12.75 0c0-.935-.201-1.823-.563-2.623l-4.65 2.772 4.39 2.986A6.35 6.35 0 0 0 16.375 10m-1.187-3.706a6.37 6.37 0 0 0-4.563-2.639v5.36z"
></path>
</svg>
`;
1 change: 1 addition & 0 deletions blocksuite/affine/blocks/database/src/views/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { viewConverts, viewPresets } from '@blocksuite/data-view/view-presets';
export const databaseBlockViews: ViewMeta[] = [
viewPresets.tableViewMeta,
viewPresets.kanbanViewMeta,
viewPresets.chartViewMeta,
];

export const databaseBlockViewMap = Object.fromEntries(
Expand Down
1 change: 1 addition & 0 deletions blocksuite/affine/data-view/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@preact/signals-core": "^1.8.0",
"@toeverything/theme": "^1.1.15",
"@types/lodash-es": "^4.17.12",
"chart.js": "^4.4.9",
"clsx": "^2.1.1",
"date-fns": "^4.0.0",
"lit": "^3.2.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import type { SingleView } from './single-view.js';

export interface ViewManager {
propertyGetOrCreate(propId: string): Property<unknown, unknown, unknown>;

Check failure on line 14 in blocksuite/affine/data-view/src/core/view-manager/view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Cannot find name 'Property'.
viewMetas: ViewMeta[];
dataSource: DataSource;
readonly$: ReadonlySignal<boolean>;
Expand All @@ -37,7 +38,7 @@
viewChangeType(id: string, type: string): void;
}

export class ViewManagerBase implements ViewManager {

Check failure on line 41 in blocksuite/affine/data-view/src/core/view-manager/view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Class 'ViewManagerBase' incorrectly implements interface 'ViewManager'.
_currentViewId$ = signal<string | undefined>(undefined);

views$ = computed(() => {
Expand Down Expand Up @@ -74,7 +75,7 @@

viewAdd(type: DataViewMode): string {
const meta = this.dataSource.viewMetaGet(type);
const data = meta.model.defaultData(this);

Check failure on line 78 in blocksuite/affine/data-view/src/core/view-manager/view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Argument of type 'this' is not assignable to parameter of type 'ViewManager'.
const id = this.dataSource.viewDataAdd({
...data,
id: nanoid(),
Expand All @@ -90,7 +91,7 @@
const meta = this.dataSource.viewMetaGet(type);
this.dataSource.viewDataUpdate(id, old => {
let data = {
...meta.model.defaultData(this),

Check failure on line 94 in blocksuite/affine/data-view/src/core/view-manager/view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Argument of type 'this' is not assignable to parameter of type 'ViewManager'.
id: old.id,
name: old.name,
mode: type,
Expand Down Expand Up @@ -125,6 +126,6 @@
viewGet(id: string): SingleView | undefined {
const meta = this.dataSource.viewMetaGetById(id);
if (!meta) return;
return new meta.model.dataViewManager(this, id);

Check failure on line 129 in blocksuite/affine/data-view/src/core/view-manager/view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Argument of type 'this' is not assignable to parameter of type 'ViewManager'.
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// AFFiNE/blocksuite/affine/data-view/src/view-presets/chart/chart-view-manager.ts

import { computed, type ReadonlySignal } from '@preact/signals-core';
import { SingleViewBase } from '../../core/view-manager/single-view.js';
import type { ViewManager } from '../../core/view-manager/view-manager.js';
import type { ChartViewData } from './define.js';
import type { Cell } from '../../core/view-manager/cell.js';

/**
* ChartSingleView manages the “Chart View” data. It computes, in a reactive
* signal, a mapping from each category value to its row‐count.
*/
export class ChartSingleView extends SingleViewBase<ChartViewData> {

Check failure on line 13 in blocksuite/affine/data-view/src/view-presets/chart/chart-view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Non-abstract class 'ChartSingleView' is missing implementations for the following members of 'SingleViewBase<ChartViewData>': 'detailProperties$', 'mainProperties$', 'properties$', 'propertiesRaw$' and 3 more.
/**
* categoryCounts$ is a computed signal that returns an object:
* { [categoryValue: string]: number }
* For each row in the datasource, it reads the cell value of
* `categoryPropertyId` and increments the corresponding count.
*/
readonly categoryCounts$: ReadonlySignal<Record<string, number>> = computed(() => {
const data = this.data$.value;
const categoryProp = data?.categoryPropertyId;
if (!categoryProp) {
return {}; // no category property selected → no data
}

// Get all existing rows in this view
const rows = this.rows$.value; // array of Row objects
const counts: Record<string, number> = {};

rows.forEach(row => {
// For each row, get/create the cell in the chosen property
const cell: Cell = this.cellGetOrCreate(row.rowId, categoryProp);
// We assume the cell’s JSON value is a string (e.g. “TODO” or “Complete”)
const raw = cell.jsonValue$.value as unknown;
const category = typeof raw === 'string' && raw.length > 0 ? raw : 'Undefined';
counts[category] = (counts[category] || 0) + 1;
});

return counts;
});

/**
* Overrides propertyGetOrCreate only if you need custom Property handling.
* For Chart, we only group by an existing property; we do not need extra property logic.
*/
override propertyGetOrCreate(propertyId: string) {
return super.propertyGetOrCreate(propertyId);

Check failure on line 48 in blocksuite/affine/data-view/src/view-presets/chart/chart-view-manager.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Abstract method 'propertyGetOrCreate' in class 'SingleViewBase<ViewData>' cannot be accessed via super expression.
}

constructor(viewManager: ViewManager, viewId: string) {
super(viewManager, viewId);
}
Comment on lines +88 to +90
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unnecessary constructor.

The constructor only calls the superclass constructor without any additional logic.

-    constructor(viewManager: ViewManager, viewId: string) {
-        super(viewManager, viewId);
-    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor(viewManager: ViewManager, viewId: string) {
super(viewManager, viewId);
}
// (constructor removed — no replacement needed)
🧰 Tools
🪛 Biome (1.9.4)

[error] 88-93: This constructor is unnecessary.

Unsafe fix: Remove the unnecessary constructor.

(lint/complexity/noUselessConstructor)

🤖 Prompt for AI Agents
In blocksuite/affine/data-view/src/view-presets/chart/chart-view-manager.ts at
lines 88 to 90, the constructor is redundant as it only calls the superclass
constructor without adding any logic. Remove the entire constructor method to
simplify the class definition and rely on the default superclass constructor
behavior.

}
28 changes: 28 additions & 0 deletions blocksuite/affine/data-view/src/view-presets/chart/define.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// AFFiNE/blocksuite/affine/data-view/src/view-presets/chart/define.ts

import type { BasicViewDataType } from '../../core/view/data-view.js';
import { viewType } from '../../core/view/data-view.js';
import { ChartSingleView } from './chart-view-manager.js';

export const chartViewType = viewType('chart');

export type ChartViewData = BasicViewDataType<
typeof chartViewType.type,
{
/** Property ID to group rows by (e.g. a “status” property). */
categoryPropertyId?: string;
}
>;

export const chartViewModel = chartViewType.createModel<ChartViewData>({
defaultName: 'Chart View',
dataViewManager: ChartSingleView,
defaultData: viewManager => {
// By default, pick the first property in the datasource as the category field (if any)
const allProps = viewManager.dataSource.properties$.value;
return {
mode: 'chart',
categoryPropertyId: allProps.length > 0 ? allProps[0] : undefined,
};
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// AFFiNE/blocksuite/affine/data-view/src/view-presets/chart/effect.ts

import { ChartViewUI } from './pc/chart-view-ui.js';

export function chartEffects() {
customElements.define('dv-chart-view-ui', ChartViewUI);
}
7 changes: 7 additions & 0 deletions blocksuite/affine/data-view/src/view-presets/chart/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
// AFFiNE/blocksuite/affine/data-view/src/view-presets/chart/index.ts

export * from './define.js';
export * from './chart-view-manager.js';
export * from './renderer.js';
export * from './effect.js';
export * from './styles.js';
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// AFFiNE/blocksuite/affine/data-view/src/view-presets/chart/pc/chart-view-ui-logic.ts

import { DataViewUILogicBase } from '../../../core/view/data-view-base.js';
import type { ChartSingleView } from '../chart-view-manager.js';
import { signal } from '@preact/signals-core';
import { ChartViewUI } from './chart-view-ui.js';
import type { Chart } from 'chart.js';

/**
* ChartViewUILogic bridges the view‐layer (ChartSingleView) and the UI (ChartViewUI).
* We hold onto a reactive signal `ui$` so our controllers can mount custom elements,
* and we keep `chartInstance` to manage Chart.js.
*/
export class ChartViewUILogic extends DataViewUILogicBase<ChartSingleView, unknown> {

Check failure on line 14 in blocksuite/affine/data-view/src/view-presets/chart/pc/chart-view-ui-logic.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Type 'unknown' does not satisfy the constraint 'DataViewSelection'.

Check failure on line 14 in blocksuite/affine/data-view/src/view-presets/chart/pc/chart-view-ui-logic.ts

View workflow job for this annotation

GitHub Actions / Typecheck

Non-abstract class 'ChartViewUILogic' is missing implementations for the following members of 'DataViewUILogicBase<ChartSingleView, unknown>': 'clearSelection', 'addRow', 'focusFirstCell', 'showIndicator' and 2 more.
/** Holds the reference to the rendered ChartViewUI element. */
ui$ = signal<ChartViewUI>();

/** Once the chart is drawn, we keep this to update or destroy as needed. */
chartInstance: Chart<'doughnut', number[], string> | null = null;

/** The `render` function used by DataView to instantiate the UI. */
renderer = (ChartViewUI as any);

/** Clean up the Chart instance when this logic is disposed. */
override onHostDisconnected(): void {

Check failure on line 25 in blocksuite/affine/data-view/src/view-presets/chart/pc/chart-view-ui-logic.ts

View workflow job for this annotation

GitHub Actions / Typecheck

This member cannot have an 'override' modifier because it is not declared in the base class 'DataViewUILogicBase<ChartSingleView, unknown>'.
if (this.chartInstance) {
this.chartInstance.destroy();
this.chartInstance = null;
}
}
}
Loading
Loading