-
-
Notifications
You must be signed in to change notification settings - Fork 3.8k
feat(editor): add "Chart View" to display items as a chart #12677
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: canary
Are you sure you want to change the base?
Changes from 8 commits
85d3f39
7c1369f
bfea0b7
1df03a4
ddbfdac
a0141fe
ef5281f
df2a759
62a7e6c
107e9bf
38f4b37
f162ff4
a908dd1
c5da0e8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,114 @@ | ||||||||||
import { computed, type ReadonlySignal } from '@preact/signals-core'; | ||||||||||
import type { InsertToPosition } from '@blocksuite/affine-shared/utils'; | ||||||||||
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'; | ||||||||||
import { PropertyBase } from '../../core/view-manager/property.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> { | ||||||||||
/** | ||||||||||
* categoryCounts$ is a computed signal that returns an object: | ||||||||||
* { [categoryValue: string]: number } | ||||||||||
* For each row in the datasource, it reads the display string of | ||||||||||
* `categoryPropertyId` and increments the corresponding count. Using the | ||||||||||
* string value ensures select properties use their tag names. | ||||||||||
*/ | ||||||||||
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); | ||||||||||
// Use the string value so Select/Multi-select show their tag names | ||||||||||
const raw = cell.stringValue$.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): ChartProperty { | ||||||||||
return new ChartProperty(this, propertyId); | ||||||||||
} | ||||||||||
|
||||||||||
/** Raw property list simply mirrors all datasource properties. */ | ||||||||||
readonly propertiesRaw$ = computed(() => { | ||||||||||
return this.dataSource.properties$.value.map(id => | ||||||||||
this.propertyGetOrCreate(id) | ||||||||||
); | ||||||||||
}); | ||||||||||
|
||||||||||
/** All properties are visible in Chart view. */ | ||||||||||
readonly properties$ = computed(() => this.propertiesRaw$.value); | ||||||||||
|
||||||||||
/** No extra detail properties beyond the normal list. */ | ||||||||||
readonly detailProperties$ = computed(() => this.properties$.value); | ||||||||||
|
||||||||||
/** Title/icon columns follow the datasource defaults. */ | ||||||||||
readonly mainProperties$ = computed(() => ({ | ||||||||||
titleColumn: this.propertiesRaw$.value.find(p => p.type$.value === 'title')?.id, | ||||||||||
iconColumn: 'type', | ||||||||||
})); | ||||||||||
|
||||||||||
/** Chart view respects the datasource readonly state. */ | ||||||||||
readonly readonly$ = computed(() => this.manager.readonly$.value); | ||||||||||
|
||||||||||
/** | ||||||||||
* The view mode string identifying this view type. | ||||||||||
* If the backing data is missing we still return 'chart'. | ||||||||||
*/ | ||||||||||
override get type(): string { | ||||||||||
return this.data$.value?.mode ?? 'chart'; | ||||||||||
} | ||||||||||
|
||||||||||
/** Display all rows. */ | ||||||||||
isShow(_rowId: string): boolean { | ||||||||||
return true; | ||||||||||
} | ||||||||||
|
||||||||||
constructor(viewManager: ViewManager, viewId: string) { | ||||||||||
super(viewManager, viewId); | ||||||||||
} | ||||||||||
Comment on lines
+88
to
+90
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
Suggested change
🧰 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
|
||||||||||
} | ||||||||||
|
||||||||||
/** Minimal property representation for Chart view. */ | ||||||||||
export class ChartProperty extends PropertyBase { | ||||||||||
/** Chart view does not support hiding columns; always visible. */ | ||||||||||
hide$ = computed(() => false); | ||||||||||
|
||||||||||
/** Hiding is ignored as properties are always visible. */ | ||||||||||
override hideSet(_hide: boolean): void { | ||||||||||
// no-op | ||||||||||
} | ||||||||||
|
||||||||||
/** | ||||||||||
* Chart view doesn\'t maintain its own ordering, so move() is a noop. | ||||||||||
* @param _position - Unused insert position. | ||||||||||
*/ | ||||||||||
override move(_position: InsertToPosition): void { | ||||||||||
// no-op | ||||||||||
} | ||||||||||
|
||||||||||
constructor(readonly chartView: ChartSingleView, propertyId: string) { | ||||||||||
super(chartView, propertyId); | ||||||||||
} | ||||||||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,43 @@ | ||
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 ChartType = 'pie' | 'bar' | 'stacked-bar' | 'line'; | ||
|
||
export type ChartViewData = BasicViewDataType< | ||
typeof chartViewType.type, | ||
{ | ||
/** Property ID to group rows by (e.g. a “status” property). */ | ||
categoryPropertyId?: string; | ||
/** How to display the chart (pie, bar, etc.). */ | ||
chartType?: ChartType; | ||
} | ||
>; | ||
|
||
export const chartViewModel = chartViewType.createModel<ChartViewData>({ | ||
defaultName: 'Chart View', | ||
dataViewManager: ChartSingleView, | ||
defaultData: viewManager => { | ||
const dataSource = viewManager.dataSource; | ||
const allProps = dataSource.properties$.value; | ||
let prop = allProps.find( | ||
id => dataSource.propertyNameGet(id) === 'Status' | ||
); | ||
if (!prop) { | ||
prop = allProps.find(id => { | ||
const type = dataSource.propertyTypeGet(id); | ||
return type === 'select' || type === 'multi-select'; | ||
}); | ||
} | ||
if (!prop && allProps.length > 0) { | ||
prop = allProps[0]; | ||
} | ||
return { | ||
mode: 'chart', | ||
categoryPropertyId: prop, | ||
chartType: 'pie', | ||
}; | ||
}, | ||
}); |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
import { ChartViewUI } from './pc/chart-view-ui.js'; | ||
|
||
export function chartEffects() { | ||
customElements.define('dv-chart-view-ui', ChartViewUI); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
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,53 @@ | ||
import { DataViewUILogicBase } from '../../../core/view/data-view-base.js'; | ||
import type { ChartSingleView } from '../chart-view-manager.js'; | ||
import { createUniComponentFromWebComponent } from '../../../core/utils/uni-component/uni-component.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> { | ||
/** 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 = createUniComponentFromWebComponent(ChartViewUI); | ||
|
||
/** Clean up the Chart instance when this logic is disposed. */ | ||
onHostDisconnected(): void { | ||
if (this.chartInstance) { | ||
this.chartInstance.destroy(); | ||
this.chartInstance = null; | ||
} | ||
} | ||
|
||
clearSelection = () => { | ||
// no selection concept in chart view | ||
}; | ||
|
||
addRow = () => { | ||
// chart view does not support adding rows | ||
return undefined; | ||
}; | ||
|
||
focusFirstCell = () => { | ||
// nothing to focus in chart view | ||
}; | ||
|
||
showIndicator = () => false; | ||
|
||
hideIndicator = () => { | ||
/* no-op */ | ||
}; | ||
|
||
moveTo = () => { | ||
/* no-op */ | ||
}; | ||
} |
Uh oh!
There was an error while loading. Please reload this page.