Skip to content
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

[Feature] Add dynamic theme switching support #20705

Merged
merged 4 commits into from
Jan 23, 2025
Merged

[Feature] Add dynamic theme switching support #20705

merged 4 commits into from
Jan 23, 2025

Conversation

Ovilia
Copy link
Contributor

@Ovilia Ovilia commented Jan 21, 2025

Brief Information

This pull request is in the type of:

  • bug fixing
  • new feature
  • others

What does this PR do?

Add support for dynamically changing themes after chart initialization.

Fixed issues

Details

Before: What was the problem?

Currently, themes can only be set before chart initialization.

echarts.registerTheme('dark', darkTheme);
const chart = echarts.init(dom, 'dark');
// You cannot change theme after chart is created unless dispose and reinit it.

There's no way to change themes dynamically after the chart is created.

The deprecated setTheme method in ECharts 3.0 was removed without a replacement.

After: How does it behave after the fixing?

Now users can:

  1. Change themes dynamically using chart.setTheme('themeName') or chart.setTheme(themeObject) for registered themes
  2. If themeName is used, both the themes registered before or after the chart was created can be applied.
  3. Theme changes are applied immediately without needing to call setOption.

Example usage:

// Using registered theme
echarts.registerTheme('dark', darkTheme);
const chart = echarts.init(dom);
chart.setTheme('dark');

// Dynamically register theme after chart is created
echarts.registerTheme('forest', forestTheme);
chart.setTheme('forest');

// The above is equivalent to:
chart.setTheme(forestTheme);
// The only difference is that forestTheme is an anonymous object, so it can't be reused for other charts.

// Using theme object directly
chart.setTheme({
    backgroundColor: '#333',
    title: {
        textStyle: {
            color: '#fff'
        }
    }
});

Document Info

One of the following should be checked.

  • This PR doesn't relate to document changes
  • The document should be updated later
  • The document changes have been made in apache/echarts-doc#xxx

Misc

ZRender Changes

  • This PR doesn't depend on ZRender changes.

Related test cases or examples to use the new APIs

  • test/theme.html

Others

Merging options

  • Please squash the commits into a single one when merging.

Other information

This PR is a pre-requisite for design token feature, with which we can dynamically change themes using design tokens.

Copy link

echarts-bot bot commented Jan 21, 2025

Thanks for your contribution!
The community will review it ASAP. In the meanwhile, please checkout the coding standard and Wiki about How to make a pull request.

The pull request is marked to be PR: author is committer because you are a committer of this project.

Document changes are required in this PR. Please also make a PR to apache/echarts-doc for document changes and update the issue id in the PR description. When the doc PR is merged, the maintainers will remove the PR: awaiting doc label.

@Ovilia Ovilia added this to the 6.0.0 milestone Jan 21, 2025
Copy link
Contributor

github-actions bot commented Jan 21, 2025

The changes brought by this PR can be previewed at: https://echarts.apache.org/examples/editor?version=PR-20705@21c5ffc

setTheme(theme: object) {
this._theme = new Model(theme);
// Merge theme with current option directly
mergeTheme(this.option, this._theme.option);
Copy link
Member

Choose a reason for hiding this comment

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

The next sentence this._resetOption('recreate', null); will recreate ecModel.option, which makes this mergeTheme ineffective.
I think it's OK to just remove this mergeTheme.
And the next sentence will call initBase to recreate a ecModel.option and merge theme to it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

OK. I tested it works without mergeTheme. Thanks.

this._model.setTheme(this._theme);
// Force refresh to apply theme changes
updateMethods.prepareAndUpdate.call(this, {
type: 'themeChanged'
Copy link
Member

Choose a reason for hiding this comment

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

I think the type: 'themeChanged' is ineffective. It won't tigger an event. it is probably inappropriate to trigger an event in an explicit API call (which might lead to dead loop).
If needing to call updateMethods.prepareAndUpdate or updateMethods.update, it's OK to use null as the 2nd parameter.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Do you mean updateMethods.prepareAndUpdate.call(this);? This causes optionChanged: payload.newOption != null to have error since payload is undefined. It works if I add payload && as:

updateMethods.update.call(this, payload, payload && {
    // Needs to mark option changed if newOption is given.
    // It's from MagicType.
    // TODO If use a separate flag optionChanged in payload?
    optionChanged: payload.newOption != null
});

Copy link
Member

@100pah 100pah Jan 22, 2025

Choose a reason for hiding this comment

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

I've changed my understanding, it's OK to use

{
    type: 'themeChanged'
}

But there is more details need to be considered, see comments below.

updateMethods.prepareAndUpdate.call(this, {
type: 'themeChanged'
});
}
Copy link
Member

Choose a reason for hiding this comment

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

If we provide an API setTheme, I think the behavior should better be similar to setOption. that is, it modifies the model, and causes re-render either immediately or in a lazy way.
But in the current implementation, re-render will not be triggered.

Copy link
Contributor Author

@Ovilia Ovilia Jan 22, 2025

Choose a reason for hiding this comment

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

By calling updateMethods.prepareAndUpdate, it will re-render as soon as calling setTheme. See the demo of test case:

onclick: function () {
    const riverTheme = {
        backgroundColor: '#ccf',
        color: ['#00f'],
        title: {
            textStyle: {
                color: '#00f'
            }
        }
    };
    chart.setTheme(riverTheme); // renders the chart at once
}

Anything else I need to change?

Copy link
Member

@100pah 100pah Jan 22, 2025

Choose a reason for hiding this comment

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

After reviewing the implementation of ECharts#resize, I updated by understanding.

  • Whether to trigger a event when an API called? The current approach is that a parameter silent is provided, which is supported by all of the ECharts#setOption, ECharts#resize and ECharts#dispatchAction. So in setTheme we'd better follow the convention.
  • Whether to call zr.flush() to perform zr render before the API return?
    • The current impl is not consistent:
      • ECharts#setOption: if not ssr, call zr.flush() immediately
      • ECharts#dispatchAction provide an parameter flush to enable customization it.
      • ECharts#resize does not call zr.flush().
    • If we do not intend to figure this inconsistency this time, I think that it's OK to follow what ECharts#resize does.
  • Whether to use a nested-call-guard: a flag IN_MAIN_PROCESS_KEY is used to prevent from nested calling in both ECharts#setOption, ECharts#resize and ECharts#dispatchAction. So I think we can follow that if we do not want to renew the impl.

So IMO the impl of setTheme can be (modified based on the impl of ECharts#resize):

export interface SetThemeOpts {
    silent?: boolean // by default false.
};


    setTheme(theme: string | ThemeOption, opts?: SetThemeOpts): void {
        if (this[IN_MAIN_PROCESS_KEY]) {
            if (__DEV__) {
                error('`setTheme` should not be called during the main process.');
            }
            return;
        }

        if (this._disposed) {
            disposedWarning(this.id);
            return;
        }

        const ecModel = this._model;
        if (!ecModel) {
            return;
        }

        let silent = opts && opts.silent;
        let updateParams = null as UpdateLifecycleParams;
        // There is some real cases that:
        // chart.setOption(option, { lazyUpdate: true });
        // chart.setTheme('some_theme');
        if (this[PENDING_UPDATE]) {
            if (silent == null) {
                silent = (this[PENDING_UPDATE] as ECharts[PENDING_UPDATE]).silent;
            }
            updateParams = (this[PENDING_UPDATE] as ECharts[PENDING_UPDATE]).updateParams;
            this[PENDING_UPDATE] = null;
        }

        this[IN_MAIN_PROCESS_KEY] = true;

        try {
            this._updateTheme(theme);
            ecModel.setTheme(this._theme);

            prepare(this);
            updateMethods.update.call(this, {type: 'setTheme'}, updateParams);
        }
        catch (e) {
            this[IN_MAIN_PROCESS_KEY] = false;
            throw e;
        }

        this[IN_MAIN_PROCESS_KEY] = false;

        flushPendingActions.call(this, silent);

        triggerUpdatedEvent.call(this, silent);
    }

BTW, it seems that there is some defects in the impl of ECharts#resize: this[PENDING_UPDATE].updateParams is omitted in lazyUpdate scenario. I've no idea whether it does that deliberately or to be a mistake.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Thanks a lot for the help. This deepens my understanding a lot.

@@ -1660,7 +1715,7 @@ class ECharts extends Eventful<ECEventDefinition> {

prepareAndUpdate(this: ECharts, payload: Payload): void {
prepare(this);
updateMethods.update.call(this, payload, {
updateMethods.update.call(this, payload, payload && {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I kept this change to make the code more robust, although we are not calling this function now.

Copy link
Member

@100pah 100pah left a comment

Choose a reason for hiding this comment

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

LGTM

@Ovilia Ovilia merged commit b5d1a83 into v6 Jan 23, 2025
2 checks passed
Copy link

echarts-bot bot commented Jan 23, 2025

Congratulations! Your PR has been merged. Thanks for your contribution! 👍

@Ovilia Ovilia deleted the feat/design-token branch January 24, 2025 10:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
PR: author is committer PR: awaiting doc Document changes is required for this PR. size/M
Projects
Status: Done
Development

Successfully merging this pull request may close these issues.

2 participants