Skip to content

Commit

Permalink
[optimize] merge Updating & Importing logic of EChart elements
Browse files Browse the repository at this point in the history
[optimize] Type Declaration of ECharts JSX props
[add] Prototype Proxy utility
  • Loading branch information
TechQuery committed Feb 16, 2024
1 parent d8a1d0f commit 50133ce
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 43 deletions.
1 change: 1 addition & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"@typescript-eslint/no-unused-vars": "warn",
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-unsafe-declaration-merging": "warn",
"@typescript-eslint/no-namespace": "warn",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/ban-ts-comment": "warn"
}
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"github-web-widget": "^4.0.0-rc.2",
"js-base64": "^3.7.6",
"koajax": "^0.9.6",
"lodash.memoize": "^4.1.2",
"marked": "^12.0.0",
"mobx": "^6.12.0",
"web-cell": "^3.0.0-rc.15",
Expand All @@ -35,6 +36,7 @@
"@parcel/transformer-less": "~2.11.0",
"@parcel/transformer-typescript-tsc": "~2.11.0",
"@parcel/transformer-webmanifest": "~2.11.0",
"@types/lodash.memoize": "^4.1.9",
"@types/node": "^18.19.15",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
Expand Down
20 changes: 20 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

75 changes: 66 additions & 9 deletions source/component/ECharts/Chart.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,30 @@
import { JsxProps } from 'dom-renderer';
import { EChartsOption } from 'echarts';
import { ECharts, init } from 'echarts/core';
import { ECBasicOption } from 'echarts/types/dist/shared';
import { CustomElement, parseDOM } from 'web-utility';

import { ChartType, loadRenderer } from './utility';
import {
BUILTIN_CHARTS_MAP,
BUITIN_COMPONENTS_MAP,
ChartType,
ECChartOptionName,
ECComponentOptionName,
loadChart,
loadComponent,
loadRenderer,
proxyPrototype
} from './utility';

export class EChartsElement extends HTMLElement implements CustomElement {
#data: EChartsOption = {};
#type: ChartType;
#core?: ECharts;
#buffer = [];

toJSON() {
return this.#core?.getOption();
}

set type(value: ChartType) {
this.#type = value;
this.setAttribute('type', value);
Expand All @@ -21,13 +38,15 @@ export class EChartsElement extends HTMLElement implements CustomElement {
constructor() {
super();

proxyPrototype(this, this.#data, (key, value) =>
this.setProperty(key.toString(), value)
);
this.attachShadow({ mode: 'open' }).append(
parseDOM('<div style="height: 100%" />')[0]
);
this.addEventListener('optionchange', ({ detail }: CustomEvent) => {
if (this.#core) this.#core.setOption(detail);
else this.#buffer.push(detail);
});
this.addEventListener('optionchange', ({ detail }: CustomEvent) =>
this.setOption(detail)
);
}

connectedCallback() {
Expand All @@ -39,16 +58,54 @@ export class EChartsElement extends HTMLElement implements CustomElement {

this.#core = init(this.shadowRoot.firstElementChild as HTMLDivElement);

for (const option of this.#buffer) this.#core.setOption(option);
this.setOption(this.#data);

for (const option of this.#buffer) this.setOption(option);

this.#buffer.length = 0;
}

async setOption(data: EChartsOption) {
if (!this.#core) {
this.#buffer.push(data);
return;
}

for (const key of Object.keys(data))
if (key in BUITIN_COMPONENTS_MAP)
await loadComponent(key as ECComponentOptionName);
else if (key in BUILTIN_CHARTS_MAP)
await loadChart(key as ECChartOptionName);

this.#core.setOption(data, false, true);
}

setProperty(key: string, value: any) {
this.#data[key] = value;

if (value != null)
switch (typeof value) {
case 'object':
break;
case 'boolean':
if (value) super.setAttribute(key, key + '');
else super.removeAttribute(key);
break;
default:
super.setAttribute(key, value + '');
}
else super.removeAttribute(key);

this.setOption(this.#data);
}
}

customElements.define('ec-chart', EChartsElement);

declare global {
interface HTMLElementTagNameMap {
'ec-chart': EChartsElement;
namespace JSX {
interface IntrinsicElements {
'ec-chart': JsxProps<EChartsElement> & ECBasicOption;
}
}
}
52 changes: 24 additions & 28 deletions source/component/ECharts/Option.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
import { JsxProps } from 'dom-renderer';
import { EChartsOption } from 'echarts';
import { CustomElement, toCamelCase, toHyphenCase } from 'web-utility';
import {
BUILTIN_CHARTS_MAP,
BUITIN_COMPONENTS_MAP,
ECChartOptionName,
ECComponentOptionName,
loadChart,
loadComponent
proxyPrototype
} from './utility';

export abstract class ECOptionElement
extends HTMLElement
implements CustomElement
{
#data = {};
#data: EChartsOption = {};

toJSON() {
return this.#data;
}

get chartTagName() {
return toCamelCase(this.tagName.split('-')[1].toLowerCase());
Expand All @@ -24,28 +30,12 @@ export abstract class ECOptionElement
constructor() {
super();

const prototype = Object.getPrototypeOf(this);

const prototypeProxy = new Proxy(prototype, {
set: (_, key, value) => {
if (typeof key === 'string') this.setProperty(key, value);
else this[key] = value;

return true;
},
get: (prototype, key, receiver) =>
key in this.#data
? this.#data[key]
: Reflect.get(prototype, key, receiver)
});

Object.setPrototypeOf(this, prototypeProxy);
proxyPrototype(this, this.#data, (key, value) =>
this.setProperty(key.toString(), value)
);
}

connectedCallback() {
if (!this.isSeries)
loadComponent(this.chartTagName as ECComponentOptionName);

this.update();
}

Expand All @@ -65,10 +55,6 @@ export abstract class ECOptionElement
}
else super.removeAttribute(key);

const { isSeries } = this;

if (isSeries && key === 'type' && value) return loadChart(value);

if (this.isConnected) this.update();
}

Expand Down Expand Up @@ -103,7 +89,17 @@ for (const name of Object.keys({
class extends ECOptionElement {}
);

type PickSingle<T> = T extends infer S | (infer S)[] ? S : T;

type ECOptionElements = {
[K in
| ECComponentOptionName
| ECChartOptionName as `ec-${K}`]: JsxProps<ECOptionElement> &
PickSingle<EChartsOption[K]>;
};

declare global {
interface HTMLElementTagNameMap
extends Record<`ec-${ECComponentOptionName}`, ECOptionElement> {}
namespace JSX {
interface IntrinsicElements extends ECOptionElements {}
}
}
41 changes: 35 additions & 6 deletions source/component/ECharts/utility.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,31 @@
import { use } from 'echarts/core';
import memoize from 'lodash.memoize';
import { IndexKey } from 'web-utility';

export function proxyPrototype<T extends object>(
target: T,
dataStore: Record<IndexKey, any>,
setter?: (key: IndexKey, value: any) => any
) {
const prototype = Object.getPrototypeOf(target);

const prototypeProxy = new Proxy(prototype, {
set: (_, key, value, receiver) => {
if (key in receiver) Reflect.set(prototype, key, value, receiver);
else dataStore[key] = value;

setter?.(key, value);

return true;
},
get: (prototype, key, receiver) =>
key in dataStore
? dataStore[key]
: Reflect.get(prototype, key, receiver)
});

Object.setPrototypeOf(target, prototypeProxy);
}

/**
* @see {@link https://github.com/apache/echarts/blob/031a908fafaa57e2277b2f720087195925ec38cf/src/model/Global.ts#L83-L111}
Expand Down Expand Up @@ -35,12 +62,12 @@ export const BUITIN_COMPONENTS_MAP = {

export type ECComponentOptionName = keyof typeof BUITIN_COMPONENTS_MAP;

export async function loadComponent(name: ECComponentOptionName) {
export const loadComponent = memoize(async (name: ECComponentOptionName) => {
const componentName = BUITIN_COMPONENTS_MAP[name];
const { [componentName]: component } = await import('echarts/components');

use(component);
}
});

/**
* @see {@link https://github.com/apache/echarts/blob/031a908fafaa57e2277b2f720087195925ec38cf/src/model/Global.ts#L113-L136}
Expand Down Expand Up @@ -70,17 +97,19 @@ export const BUILTIN_CHARTS_MAP = {
custom: 'CustomChart'
} as const;

export async function loadChart(name: keyof typeof BUILTIN_CHARTS_MAP) {
export type ECChartOptionName = keyof typeof BUILTIN_CHARTS_MAP;

export const loadChart = memoize(async (name: ECChartOptionName) => {
const chartName = BUILTIN_CHARTS_MAP[name];
const { [chartName]: chart } = await import('echarts/charts');

use(chart);
}
});

export type ChartType = 'svg' | 'canvas';

export async function loadRenderer(type: ChartType) {
export const loadRenderer = memoize(async (type: ChartType) => {
const { SVGRenderer, CanvasRenderer } = await import('echarts/renderers');

use(type === 'svg' ? SVGRenderer : CanvasRenderer);
}
});

1 comment on commit 50133ce

@github-actions
Copy link
Contributor

Choose a reason for hiding this comment

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

Deploy preview for wuhan2020 ready!

✅ Preview
https://wuhan2020-cwy1tjfv2-techquery.vercel.app

Built with commit 50133ce.
This pull request is being automatically deployed with vercel-action

Please sign in to comment.