At the moment, updating/refactoring the elastic-charts code base includes the risk of easily introducing circular dependencies. Resolving these can be time consuming and it makes PRs larger than necessary because of the required additional refactorings.
This issue tries to summarize approaches that can be used to reduce the risk of running into circular dependencies.
What is causing the risk?
The code base is organized in more general "base" code under packages/charts/src/state and specific code for different chart types in packages/charts/src/chart_types. Just as an analogy (I'm not suggesting refactoring anything towards being more OOP), in classic OOP we could consider the state code the classes and chart_types the implementations or instanced classes. The important bit is that the relationship is unidirectional state -> chart_types. The current code base handles this at just a very informal level and there are no restrictions in place to avoid two-way-dependencies. One of the main culprits is that what's called internal state at the moment is reimporting everything from chart_types to set up the chart specific renderers and selectors.
Analysis with dependency-cruiser
Once you start refactoring and run into a circular dependency it's hard to find the actual root cause. dependency-cruiser is a library to help with this and can point you to the exact files causing the problems, for example:
warn no-circular:
packages/charts/src/chart_types/heatmap/state/chart_state.ts →
packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts →
packages/charts/src/chart_types/heatmap/state/selectors/compute_axes_sizes.ts →
packages/charts/src/state/selectors/compute_small_multiple_scales.ts →
packages/charts/src/state/selectors/get_internal_main_projection_area.ts →
packages/charts/src/state/selectors/get_internal_chart_state.ts →
packages/charts/src/chart_types/heatmap/state/chart_state.ts
On top of this out-of-the-box-feature, one can describe custom rules for the validation. For example, we might want to warn or error out when someone tries to import from chart_types into state:
{
name: 'two-way-chart-type-not-allowed',
severity: 'error',
from: { path: '^packages/charts/src/state' },
to: { path: '^packages/charts/src/chart_types' },
}
$ npx depcruise packages/charts/src
...
error two-way-chart-type-not-allowed: packages/charts/src/state/utils.test.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/spec_factory.test.tsx → packages/charts/src/chart_types/xy_chart/specs/bar_series.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/is_external_tooltip_visible.ts → packages/charts/src/chart_types/xy_chart/state/selectors/get_computed_scales.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/get_tooltip_spec.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/get_small_multiples_spec.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/get_small_multiples_index_order.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/get_settings_spec.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/can_pin_tooltip.ts → packages/charts/src/chart_types/xy_chart/state/selectors/get_tooltip_values_highlighted_geoms.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/selectors/can_pin_tooltip.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/reducers/interactions.ts → packages/charts/src/chart_types/partition_chart/state/selectors/picked_shapes.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/reducers/interactions.ts → packages/charts/src/chart_types/partition_chart/state/selectors/drilldown_active.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/reducers/interactions.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/xy_chart/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/wordcloud/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/timeslip/internal_chart_state.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/partition_chart/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/metric/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/index.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/heatmap/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/goal_chart/state/chart_state.tsx
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/flame_chart/internal_chart_state.ts
error two-way-chart-type-not-allowed: packages/charts/src/state/chart_state.ts → packages/charts/src/chart_types/bullet_graph/chart_state.tsx
Besides validation and analysis problems at hand, dependency-cruiser can generate charts that visualize the dependency graph:

Note that this includes the custom rule we defined and highlights import from chart_types to state in red!
Note that the rules could also be defined using eslint.
linting imports/exports
Barrel files can be problematic in relation to circular dependencies too. (Background: https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/)
export * from "./foo";
export * from "./bar";
export * from "./baz";
// stuff like this is esp. tricky:
export * from "../some-other-dir-further-up";
linting type imports
This could improve readability and avoid some issues with imports. In Kibana some plugins enforce linting to have import type { .
refactor internal chart state
To avoid circular dependencies between state and chart_types, we can refactor InternalChartState:
- break out the renderers to be only imported from
chart/chart_container.tsx
- break out the selectors into a dynamic registry.
getInternalChartState would only define the registry, when setting up the store we can initialize the registry.
task summary
Potential tasks to improve code base related to circular dependencies:
At the moment, updating/refactoring the
elastic-chartscode base includes the risk of easily introducing circular dependencies. Resolving these can be time consuming and it makes PRs larger than necessary because of the required additional refactorings.This issue tries to summarize approaches that can be used to reduce the risk of running into circular dependencies.
What is causing the risk?
The code base is organized in more general "base" code under
packages/charts/src/stateand specific code for different chart types inpackages/charts/src/chart_types. Just as an analogy (I'm not suggesting refactoring anything towards being more OOP), in classic OOP we could consider thestatecode the classes andchart_typesthe implementations or instanced classes. The important bit is that the relationship is unidirectionalstate -> chart_types. The current code base handles this at just a very informal level and there are no restrictions in place to avoid two-way-dependencies. One of the main culprits is that what's calledinternal stateat the moment is reimporting everything fromchart_typesto set up the chart specific renderers and selectors.Analysis with
dependency-cruiserOnce you start refactoring and run into a circular dependency it's hard to find the actual root cause. dependency-cruiser is a library to help with this and can point you to the exact files causing the problems, for example:
warn no-circular: packages/charts/src/chart_types/heatmap/state/chart_state.ts → packages/charts/src/chart_types/heatmap/state/selectors/compute_chart_dimensions.ts → packages/charts/src/chart_types/heatmap/state/selectors/compute_axes_sizes.ts → packages/charts/src/state/selectors/compute_small_multiple_scales.ts → packages/charts/src/state/selectors/get_internal_main_projection_area.ts → packages/charts/src/state/selectors/get_internal_chart_state.ts → packages/charts/src/chart_types/heatmap/state/chart_state.tsOn top of this out-of-the-box-feature, one can describe custom rules for the validation. For example, we might want to warn or error out when someone tries to import from
chart_typesintostate:Besides validation and analysis problems at hand,
dependency-cruisercan generate charts that visualize the dependency graph:Note that this includes the custom rule we defined and highlights import from
chart_typestostatein red!Note that the rules could also be defined using
eslint.linting imports/exports
Barrel files can be problematic in relation to circular dependencies too. (Background: https://marvinh.dev/blog/speeding-up-javascript-ecosystem-part-7/)
linting
typeimportsThis could improve readability and avoid some issues with imports. In Kibana some plugins enforce linting to have
import type {.refactor internal chart state
To avoid circular dependencies between
stateandchart_types, we can refactorInternalChartState:chart/chart_container.tsxgetInternalChartStatewould only define the registry, when setting up the store we can initialize the registry.task summary
Potential tasks to improve code base related to circular dependencies:
dependency-cruiseras part of the repo.chore(deps): dependency cruiser #2616
import type ...chore(types): consistent type imports #2618
chore(refactor): refactor chart specific selectors out of state #2613