the in the v3 version the enhanced Chart module is exported as the solely (default) export from the library.
https://github.com/chartjs/Chart.js/blob/master/src/index.js
As an alternative one could have the following structure:
import ChartChart from './core/core.controller';
export { default as Chart } from './core/core.controller';
import DatasetController from './core/core.datasetController';
export { default as DatasetController } from './core/core.datasetController';
class Chart extends ChartChart {
}
Chart.DatasetController = DatasetController;
export default Chart;
the purpose around the Chart class is that only the default export has all the dependencies but not the class itself.
I want to prepare for tree shaking and thus only import things that I really need. e.g.,
import {DatasetController} from 'chart.js';
class MyController extends DatasetController {
..}
note: while this works already with some synthetic imports (by rollup, ...) it is not standard conform.
I would love to make Chart.js tree-shakeable
We had investigated a bit earlier. Some notes:
npm that is tree-shakeablesideEffects: false is set.'time' or 'line'. I believe @etimberg suggested we keep it this waysideEffects: false. I think deciding on this last point is the only real thing we need to do before implementing tree shaking.@sgratzl any chance you would want to implement this? :-) We're all pretty busy and haven't had much time for core development lately, but we could talk and decide that last point if someone has the time to implement it
Some quick opinions:
Core or ChartCore or CoreChart rather than ChartChart.I am in favour of the string API if possible. I think its a better default out of the box since it's backwards compatible with v2 code and also quite easy to understand. It also doesn't break any existing documentation that's out there on SO or equivalent.
If we can do an opt-in tree shaking API initially, I am on-board. That would let us experiment and see what works well and then consider how to migrate all users to it eventually.
Ok, I think basically all we need to do is:
I think the biggest side effect in the library are the defaults. e.g., while elements don't need to be registered their defaults need to be, Thus creating a side effect. One approach could be using the same idea as in the scale case ->static .defaults attribute.
in https://github.com/sgratzl/chartjs-chart-boxplot/tree/develop I adapted the plugin to support tree shaking as good as possible. Essential changes:
.id, .defaults. and .register. The .register function registers it within chart.js creating side effects. Moreover, when registering a controller, it also registers all its default elements and scales. Chart chart which injects the type into its config. For example, the BoxPlotChart can be used to create a boxplot with implicitly registered the BoxPlotController. see also https://github.com/sgratzl/chartjs-chart-boxplot/blob/develop/samples/default_esm.html* It would be good to create a generic register method that can take scales and controllers so users can register everything they need in one line
I would prefer a function per type. it is easier to ensure that the right arguments are given and one can avoid conflicts in names. e.g. a Violin controller and a Violin element.
* Create a new entry point for npm / esm users
I have a bundle.js and index.js where the bundle.js is like the esm index.js but registers everything for the UMD case.
I am in favour of the string API if possible. I think its a better default out of the box since it's backwards compatible with v2 code and also quite easy to understand. It also doesn't break any existing documentation that's out there on SO or equivalent.
in the best case, chart.js supports two options everywhere where a .type is required:
relevant locations as far as I can see:
scales.x.type = 'category'.type = 'bar'BarController.prototype.dataElementType = RectangleI would prefer a function per type. it is easier to ensure that the right arguments are given and one can avoid conflicts in names. e.g. a Violin controller and a Violin element.
If we had one method it could check the type (e.g. is it a subclass of DatasetController or Scale) and still store it in a separate registry based on type
I have a bundle.js and index.js where the bundle.js is like the esm index.js but registers everything for the UMD case.
That sounds good to me
in the best case, chart.js supports two options everywhere where a .type is required:
the string id that will be looked up in the registry
a constructor / function that is used to create the instance directly.
We actually only need the constructor. At least that's how scale registration works now. The id is a static property on the constructor
in the best case, chart.js supports two options everywhere where a .type is required
We had discussed this as an option earlier, but I think the consensus was to only support the string option via registration in v3 because we were worried it would be too confusing to have two ways to do things and would confuse people when they were looking at samples on StackOverflow, etc.
I played a bit with tree shaking by manually commenting out things from the build that are not needed for a bar chart and then use rollup to remove all the things that fall of. see https://github.com/sgratzl/chartjs-treeshake-test.
in summary:
it could be a bit less if the helpers object is not used within the code but just the methods directly. Then, some parts of the helpers object could be shaken out, e.g., in this example splineCurve and so on.
besides removing all the wrappers (elements, plugins, scales, ...) I mostly had to comment out access of the prototypes (e.g., LineController.prototype.dataElementOptions = ...) and manipulating the defaults object, (e..g, defaults.set('line', ....). The former prevents the LineController from being shaken since it is actually used and the later since its config is not needed.
Wow, that's great! I'm a little surprised that not more was able to be shaken out, but it's a good start. No one will complain about a 30% reduction in build size! If you want to send us any PRs to make these changes we'd be happy to review. (btw, I didn't understand what you meant about wrappers)
Code tends to shake out better when it's functions. We use a lot of classes and those don't shake out well, so I guess that's the core issue. I think there are some additional wins we can get by updating things a bit.
E.g. one I noticed is that the whole color library is getting pulled in and we don't need most of it. We really just use mix in core.animation. It's class-based like the rest of our code and that class depends on most of the other files in the project. However, if we updated it to be just a collection of functions like date-fns then we could shake most of it out. That might require some changes to our API. E.g. we have code to parse any type of color that a user gives to us right now. It might be better to take only rgba and then if a user wants to provide something like hsl, they can import the color library and do the parsing themselves. That would save it from being imported into the build for every user regardless of whether they use that functionality.
Another thing I noticed is that your build had the ResizeObserver polyfill in it. That shouldn't be present in our ES6 builds (we'd expect users to provide their own polyfill if they want to support older browsers), so hopefully we could find a way to remove it as well
Wow, that's great! I'm a little surprised that not more was able to be shaken out, but it's a good start. No one will complain about a 30% reduction in build size! If you want to send us any PRs to make these changes we'd be happy to review. (btw, I didn't understand what you meant about wrappers)
Code tends to shake out better when it's functions. We use a lot of classes and those don't shake out well, so I guess that's the core issue. I think there are some additional wins we can get by updating things a bit.
the main question I wonder is how to deal with detault values. e.g. all the defaults.set(...) calls that are not bound to the class and thus not shaken out. Moreover, in two or three locations (legend, title, ...) the defaults.elements.line are used, even tho I don't know how those are related to the context.
E.g. one I noticed is that the whole color library is getting pulled in and we don't need most of it. We really just use
mixincore.animation. It's class-based like the rest of our code and that class depends on most of the other files in the project. However, if we updated it to be just a collection of functions likedate-fnsthen we could shake most of it out. That might require some changes to our API. E.g. we have code to parse any type of color that a user gives to us right now. It might be better to take only rgba and then if a user wants to provide something like hsl, they can import the color library and do the parsing themselves. That would save it from being imported into the build for every user regardless of whether they use that functionality.
one could consider switching to a different library such as d3-color or tinycolor. regarding custom parsing, it would require that uses can manipulate the interpolators or the automatic hover color.
Another thing I noticed is that your build had the
ResizeObserverpolyfill in it. That shouldn't be present in our ES6 builds (we'd expect users to provide their own polyfill if they want to support older browsers), so hopefully we could find a way to remove it as well
if you take a look at https://unpkg.com/[email protected]/dist/Chart.esm.js?module it is part of your esm build. maybe babel injects it because of your browserlist settings.
if you take a look at https://unpkg.com/[email protected]/dist/Chart.esm.js?module it is part of your esm build. maybe babel injects it because of your browserlist settings.
https://www.chartjs.org/dist/master/Chart.esm.js does not contain it anymore.
the main question I wonder is how to deal with detault values. e.g. all the defaults.set(...) calls that are >not bound to the class and thus not shaken out.
Maybe apply the defaults when registering the controller / element. Part of the class prototype? Not sure where the best location to ship those is.
Moreover, in two or three locations (legend, title, ...) the defaults.elements.line are used, even tho I don't know how those are related to the context.
It looks like just the legend has that issue. We had at one point talked about removing the element options. Regardless though, I would be open to giving the legend its own defaults instead of using defaults.elements.line. As a temporary workaround we could just import the LineElement into the entry point to ensure those defaults are available and unblock the rest of the tree shaking
Maybe apply the defaults when registering the controller / element.
That sounds good to me.
Part of the class prototype?
@sgratzl had said earlier "I mostly had to comment out access of the prototypes (e.g., LineController.prototype.dataElementOptions = ...). [They prevent the] LineController from being shaken since it is actually used". I'm not sure if there's a better way to do this. Does it work if we make them static fields?
I have seen that most imports are leaving out the file extension.
While this is fine for webpack/rollup or any other bundler that tries out some extension etc. in their resolver logic, this won't work for direct node or browser imports.
See https://nodejs.org/api/esm.html#esm_mandatory_file_extensions, "Mandatory file extensions".
I have seen that most imports are leaving out the file extension.
While this is fine for webpack/rollup or any other bundler that tries out some extension etc. in their resolver logic, this won't work for direct node or browser imports.
See https://nodejs.org/api/esm.html#esm_mandatory_file_extensions, "Mandatory file extensions".
I'm not sure why this is relevant, we are using rollup and babel to transpile the stuff before publishing it to npm.
one general point which there needs to be a consensus about before continuing:
There are two common ways how to deal with tree-shaking:
export * from or export {default as ... and rollup creates a single esm file containing everything.import what you want approach. So, instead of a remove what you don't need to add what you need. It requires that the build is not just one file but all the files that should be importable. e.g., import BarController from 'chart.js/controllers/bar requires that the npm package has a file: controllers/bar.js.The separted case can be extended to also support the flat case but requires to have the sideEffects false flag to be set.
pros/cons
src/controllers/controller.bar.js to controllers/bar.jsimport {clear} from 'chart.js/helpers/canvas@sgratzl take a look at #7400, what do you think about that one?
Everything else is flat, except helpers.
For import {isObject} from 'chart.js/helpers/core'; to work, I moved the outputs to root (from dist).
If not moved, it would need to be import {isObject} from 'chart.js/dist/helpers/core';
@sgratzl had said earlier "I mostly had to comment out access of the prototypes (e.g., LineController.prototype.dataElementOptions = ...). [They prevent the] LineController from being shaken since it is actually used". I'm not sure if there's a better way to do this. Does it work if we make them static fields?
Maybe those (dataElementOptions / datasetElementOptions) should also be part of the registration process.
Edit: The static fields are just some syntax sugar afaik, so those won't really help.
I wouldn't say that you have to choose between either a single bundle file or direct delivery of the source files, but you could do both. I'm not sure how common this is yet, but I see it with most of the modules we work with (VueJS, Vuetify, ...).
Taking a note from #7400
no idea really. could be referenced directly from browser, i think?
Importing the source files directly from a CDN via <script type="module" src="chartjs.js"></script> (where chartjs.js is not a bundle, but imports other needed files itself) can also make use of caching and will only reload changed files, which would not be possible with a bundle. It also eases bug reports, as we would got the same file/line reported in user land as in this repo (not needing any mapping files anymore).
I wouldn't say that a (single-file) bundle is an old artifact, now that JS is capable of module-support, but I see its main use only in supporting old browsers that are incapable of type="module" as well as older NodeJS versions (before v13.2) where ESM support was still experimental.
In addition, users who use their own Webpack/Rollup/... bundle-process have also no need for a pre-bundled version, regardless which NodeJS version they are using or which browsers they want to support.
In summary, it would be great if both could be supported (adding the source folder to the npm package and using the full file name in the imports).
what about splitting up this issue, this one contains the discussion about how to structure the ESM build and another one about how to do the registration process for different components: controller, scales, elements, plugins
@sgratzl ready to close this one?
it would be nice if the logic and methods of this code block is generalized and also available in the ESM build:
https://github.com/chartjs/Chart.js/blob/426d8debba3acc89525bd4369b11620b94284e04/src/index.js#L28-L38
Hi 😃 !
Just curious, do you have an approximate release "date/month" for tree-shaking? (I guess it's on your spare time, but just curious if any).
Thank you,
any day now
Yes, we're quite close. Barring any last minute issues, we'll probably do the v3.0.0-alpha.2 release tonight or tomorrow
Separating the helpers in the ESM build has broken chartjs-adapter-luxon and chartjs-adapter-datefns because they expect helpers to be a property on the chart.js export: https://github.com/chartjs/chartjs-adapter-luxon/blob/master/src/index.js#L34
I've sent fixes for the date adapters:
https://github.com/chartjs/chartjs-adapter-luxon/pull/20
https://github.com/chartjs/chartjs-adapter-date-fns/pull/16
In the meantime, you can avoid any issues by supplying timestamps and setting parsing: false so that the parse method in the date adapters is not called
@sgratzl are there any more changes, you'd like to the export logic of v3?
@sgratzl are there any more changes, you'd like to the export logic of v3?
no, looking great, thx to all for all the effort
Most helpful comment
I've sent fixes for the date adapters:
https://github.com/chartjs/chartjs-adapter-luxon/pull/20
https://github.com/chartjs/chartjs-adapter-date-fns/pull/16
In the meantime, you can avoid any issues by supplying timestamps and setting
parsing: falseso that theparsemethod in the date adapters is not called