@vuu-ui/vuu-chart 4.2.0 → 4.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "4.2.0",
2
+ "version": "4.2.2",
3
3
  "description": "support for apache echarts using vuu data",
4
4
  "main": "./src/index.js",
5
5
  "license": "Apache-2.0",
@@ -8,16 +8,16 @@
8
8
  "type-defs": "node ../../scripts/build-type-defs.mjs"
9
9
  },
10
10
  "devDependencies": {
11
- "@vuu-ui/vuu-data-types": "4.2.0",
12
- "@vuu-ui/vuu-protocol-types": "4.2.0",
13
- "@vuu-ui/vuu-data-local": "4.2.0"
11
+ "@vuu-ui/vuu-data-types": "4.2.2",
12
+ "@vuu-ui/vuu-protocol-types": "4.2.2",
13
+ "@vuu-ui/vuu-data-local": "4.2.2"
14
14
  },
15
15
  "dependencies": {
16
16
  "@salt-ds/styles": "0.4.2",
17
17
  "@salt-ds/window": "0.1.2",
18
- "@vuu-ui/vuu-context-menu": "4.2.0",
19
- "@vuu-ui/vuu-ui-controls": "4.2.0",
20
- "@vuu-ui/vuu-utils": "4.2.0",
18
+ "@vuu-ui/vuu-context-menu": "4.2.2",
19
+ "@vuu-ui/vuu-ui-controls": "4.2.2",
20
+ "@vuu-ui/vuu-utils": "4.2.2",
21
21
  "clsx": "^2.0.0",
22
22
  "echarts": "6.1.0"
23
23
  },
package/src/Chart.css.js CHANGED
@@ -26,6 +26,7 @@ const css = `
26
26
  background-color: var(--salt-container-primary-background) !important;
27
27
  border-color: var(--salt-selectable-borderColor-selected) !important;
28
28
  font-family: var(--salt-text-fontFamily) !important;
29
+ z-index: var(--salt-zIndex-popout) !important;
29
30
  border-radius: 6px !important;
30
31
 
31
32
  & .vuuChart-tooltip-title {
package/src/Chart.js CHANGED
@@ -5,18 +5,24 @@ import { MeasuredContainer } from "@vuu-ui/vuu-ui-controls";
5
5
  import { buildColumnMap } from "@vuu-ui/vuu-utils";
6
6
  import clsx from "clsx";
7
7
  import { getInstanceByDom, init } from "./echarts.js";
8
- import { useCallback, useEffect, useRef, useState } from "react";
8
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
9
9
  import { useChartContextMenu } from "./useChartContextMenu.js";
10
10
  import { useChartOptions } from "./useChartOptions.js";
11
11
  import { useChartSelection } from "./useChartSelection.js";
12
12
  import Chart from "./Chart.css";
13
13
  const classBase = "vuuChart";
14
- const Chart_Chart = ({ categoryColumnName, chartSettings = {
14
+ const defaultOptionSettings = {
15
+ notMerge: false
16
+ };
17
+ const seriesChangedOptionSettings = {
18
+ notMerge: true
19
+ };
20
+ const seriesIds = (option)=>(option.series ?? []).map(({ id })=>id).join("|");
21
+ const defaultChartSettings = {
15
22
  useCoarsePointer: true,
16
23
  renderer: "svg"
17
- }, className, config, dataExclusions, dataSource, height, optionSettings = {
18
- notMerge: true
19
- }, palette, resizeStrategy, showTooltip, style = {
24
+ };
25
+ const Chart_Chart = ({ categoryColumnName, chartSettings = defaultChartSettings, className, config, dataExclusions, dataSource, height, optionSettings = defaultOptionSettings, palette, resizeStrategy, showTooltip, style = {
20
26
  width: "100%",
21
27
  height: "100%"
22
28
  }, seriesColumnNames, width, ...htmlAttributes })=>{
@@ -38,8 +44,10 @@ const Chart_Chart = ({ categoryColumnName, chartSettings = {
38
44
  width: size.width
39
45
  });
40
46
  }, []);
41
- const columnMap = buildColumnMap(dataSource.columns);
42
- const onContextMenu = useChartContextMenu({
47
+ const columnMap = useMemo(()=>buildColumnMap(dataSource.columns), [
48
+ dataSource.columns
49
+ ]);
50
+ const { onContextMenu, onMouseMove } = useChartContextMenu({
43
51
  categoryColumnName,
44
52
  columnMap
45
53
  });
@@ -58,30 +66,60 @@ const Chart_Chart = ({ categoryColumnName, chartSettings = {
58
66
  symbolSizeFunction,
59
67
  showTooltip
60
68
  });
69
+ const { renderer = "svg", useCoarsePointer = true } = chartSettings;
61
70
  useEffect(()=>{
62
71
  if (!chartElement) return;
63
- const chart = init(chartElement, null, chartSettings);
72
+ const chart = init(chartElement, null, {
73
+ renderer,
74
+ useCoarsePointer
75
+ });
64
76
  chartElement.echartsInstance = chart;
77
+ return ()=>{
78
+ chart?.dispose();
79
+ delete chartElement.echartsInstance;
80
+ };
81
+ }, [
82
+ chartElement,
83
+ renderer,
84
+ useCoarsePointer
85
+ ]);
86
+ useEffect(()=>{
87
+ const chart = chartElement ? getInstanceByDom(chartElement) : void 0;
88
+ if (!chart) return;
65
89
  chart.on("contextmenu", onContextMenu);
66
90
  chart.on("click", onClick);
67
91
  chart.on("mouseover", onMouseOver);
68
92
  chart.on("mouseout", onMouseOut);
93
+ chart.getZr().on("mousemove", onMouseMove);
69
94
  return ()=>{
70
- chart?.dispose();
71
- delete chartElement.echartsInstance;
95
+ if (!chart.isDisposed()) {
96
+ chart.getZr().off("mousemove", onMouseMove);
97
+ chart.off("contextmenu", onContextMenu);
98
+ chart.off("click", onClick);
99
+ chart.off("mouseover", onMouseOver);
100
+ chart.off("mouseout", onMouseOut);
101
+ }
72
102
  };
73
103
  }, [
74
104
  chartElement,
75
- chartSettings,
76
105
  onClick,
77
106
  onContextMenu,
107
+ onMouseMove,
78
108
  onMouseOver,
79
- onMouseOut
109
+ onMouseOut,
110
+ renderer,
111
+ useCoarsePointer
80
112
  ]);
113
+ const seriesIdsRef = useRef(void 0);
81
114
  useEffect(()=>{
82
115
  if (chartElement) {
83
116
  const chart = getInstanceByDom(chartElement);
84
- chart?.setOption(option, optionSettings);
117
+ if (chart) {
118
+ const ids = seriesIds(option);
119
+ const seriesChanged = ids !== seriesIdsRef.current;
120
+ seriesIdsRef.current = ids;
121
+ chart.setOption(option, seriesChanged ? seriesChangedOptionSettings : optionSettings);
122
+ }
85
123
  }
86
124
  }, [
87
125
  chartElement,
@@ -2,6 +2,7 @@ import { EventEmitter, Range, buildColumnMap, metadataKeys } from "@vuu-ui/vuu-u
2
2
  import { MovingDataRowWindow } from "./DataRowMovingWindow.js";
3
3
  import { defaultItemColorFunction } from "./chart-types.js";
4
4
  const { KEY: KEY } = metadataKeys;
5
+ const CHART_RANGE = Range(0, 1000);
5
6
  function DataSourceValue(key, row, value) {
6
7
  return {
7
8
  key,
@@ -17,7 +18,8 @@ class ChartSeries extends EventEmitter {
17
18
  #dataExclusions;
18
19
  #dataSource;
19
20
  #subscribed = false;
20
- #dataWindow = new MovingDataRowWindow(Range(0, 1000));
21
+ #previousRange;
22
+ #dataWindow = new MovingDataRowWindow(CHART_RANGE);
21
23
  #dates = [];
22
24
  #palette;
23
25
  #series = [];
@@ -47,22 +49,36 @@ class ChartSeries extends EventEmitter {
47
49
  this.#dataExclusions = dataExclusions;
48
50
  this.#palette = palette;
49
51
  this.#dataSource = dataSource;
50
- this.subscribe();
52
+ }
53
+ get dataSource() {
54
+ return this.#dataSource;
51
55
  }
52
56
  subscribe() {
53
- if (!this.#subscribed) {
54
- this.#subscribed = true;
55
- this.#dataSource.subscribe({
56
- range: Range(0, 1000)
57
- }, this.handleData);
57
+ if (this.#subscribed) return;
58
+ this.#subscribed = true;
59
+ const dataSource = this.#dataSource;
60
+ const { status } = dataSource;
61
+ if ("initialising" === status || "unsubscribed" === status) dataSource.subscribe({
62
+ range: CHART_RANGE
63
+ }, this.handleData);
64
+ else {
65
+ const columns = dataSource.columns.length > 0 ? dataSource.columns : dataSource.tableSchema?.columns;
66
+ this.#columnMap = buildColumnMap(columns);
67
+ this.#previousRange = dataSource.range;
68
+ dataSource.range = CHART_RANGE;
69
+ if ("subscribed" === status) dataSource.suspend?.(false);
70
+ dataSource.resume?.(this.handleData);
58
71
  }
59
72
  }
60
- detach() {
61
- this.#subscribed = false;
62
- }
63
- dispose() {
73
+ release(escalateToDisable) {
74
+ if (!this.#subscribed) return;
64
75
  this.#subscribed = false;
65
- this.#dataSource.unsubscribe();
76
+ const dataSource = this.#dataSource;
77
+ dataSource.suspend?.(escalateToDisable);
78
+ if (this.#previousRange) {
79
+ dataSource.range = this.#previousRange;
80
+ this.#previousRange = void 0;
81
+ }
66
82
  }
67
83
  get palette() {
68
84
  return this.#seriesColumns.map((col)=>this.#colorMap.get(col));
@@ -79,8 +95,38 @@ class ChartSeries extends EventEmitter {
79
95
  }
80
96
  set seriesColumnNames(seriesColumnNames) {
81
97
  this.#seriesColumns = seriesColumnNames;
98
+ this.assignColors();
82
99
  if (this.#categoryColumn) this.buildCategoriesAndSeries();
83
100
  }
101
+ assignColors() {
102
+ const colorMap = this.#colorMap;
103
+ const seriesColumns = this.#seriesColumns;
104
+ const count = seriesColumns.length;
105
+ if (0 === count || 0 === this.#palette.length) return;
106
+ const runtimePalette = Array.from({
107
+ length: count
108
+ }, (_, i)=>this.#palette[i % this.#palette.length]);
109
+ const usedColors = new Set();
110
+ for (const column of seriesColumns){
111
+ const color = colorMap.get(column);
112
+ if (void 0 !== color) usedColors.add(color);
113
+ }
114
+ seriesColumns.forEach((column, preferredSlot)=>{
115
+ if (!colorMap.has(column)) {
116
+ let slot = preferredSlot;
117
+ for(let i = 0; i < count; i++){
118
+ const candidate = (preferredSlot + i) % count;
119
+ if (!usedColors.has(runtimePalette[candidate])) {
120
+ slot = candidate;
121
+ break;
122
+ }
123
+ }
124
+ const color = runtimePalette[slot];
125
+ usedColors.add(color);
126
+ colorMap.set(column, color);
127
+ }
128
+ });
129
+ }
84
130
  get categories() {
85
131
  return this.#dates;
86
132
  }
@@ -88,7 +134,6 @@ class ChartSeries extends EventEmitter {
88
134
  return this.#series;
89
135
  }
90
136
  getCategoriesAndSeries() {
91
- const colorMap = this.#colorMap;
92
137
  const columnMap = this.#columnMap;
93
138
  const data = this.#dataWindow.data;
94
139
  const categoryColumn = this.#categoryColumn;
@@ -106,13 +151,15 @@ class ChartSeries extends EventEmitter {
106
151
  const seriesTuple = seriesMap.get(seriesColumn);
107
152
  if (seriesTuple) [lineSeries, scatterSeries] = seriesTuple;
108
153
  else {
109
- if (!colorMap.has(seriesColumn)) colorMap.set(seriesColumn, this.#palette[this.#colorMap.size]);
110
154
  lineSeries = {
111
155
  id: seriesColumn,
112
156
  name: seriesColumn,
113
157
  label: seriesColumn,
114
158
  connectNulls: true,
115
159
  data: [],
160
+ emphasis: {
161
+ disabled: true
162
+ },
116
163
  type: "line",
117
164
  itemStyle: {
118
165
  color: itemColorFunction
@@ -1,7 +1,10 @@
1
- import { useContextMenu } from "@vuu-ui/vuu-context-menu";
2
- import { useCallback } from "react";
1
+ import { useContextMenu, useHideContextMenu } from "@vuu-ui/vuu-context-menu";
2
+ import { useCallback, useRef } from "react";
3
+ const CLOSE_MENU_DISTANCE = 8;
3
4
  const useChartContextMenu = ({ columnMap })=>{
4
5
  const showContextMenu = useContextMenu();
6
+ const hideContextMenu = useHideContextMenu();
7
+ const menuPositionRef = useRef(void 0);
5
8
  const onContextMenu = useCallback(({ data, event, seriesName })=>{
6
9
  if (event && seriesName) {
7
10
  event?.stop();
@@ -11,12 +14,38 @@ const useChartContextMenu = ({ columnMap })=>{
11
14
  columnMap,
12
15
  row
13
16
  };
14
- showContextMenu(event.event, "series", menuOptions);
17
+ const mouseEvent = event.event;
18
+ const onOpenChange = (open)=>{
19
+ if (!open) menuPositionRef.current = void 0;
20
+ };
21
+ const shown = showContextMenu(mouseEvent, "series", menuOptions, {
22
+ onOpenChange
23
+ });
24
+ if (shown) menuPositionRef.current = {
25
+ x: mouseEvent.clientX,
26
+ y: mouseEvent.clientY
27
+ };
15
28
  }
16
29
  }, [
17
30
  columnMap,
18
31
  showContextMenu
19
32
  ]);
20
- return onContextMenu;
33
+ const onMouseMove = useCallback(({ event })=>{
34
+ const menuPosition = menuPositionRef.current;
35
+ if (menuPosition) {
36
+ const { clientX, clientY } = event;
37
+ const distance = Math.hypot(clientX - menuPosition.x, clientY - menuPosition.y);
38
+ if (distance > CLOSE_MENU_DISTANCE) {
39
+ menuPositionRef.current = void 0;
40
+ hideContextMenu();
41
+ }
42
+ }
43
+ }, [
44
+ hideContextMenu
45
+ ]);
46
+ return {
47
+ onContextMenu,
48
+ onMouseMove
49
+ };
21
50
  };
22
51
  export { useChartContextMenu };
@@ -8,7 +8,21 @@ const defaultPalette = [
8
8
  "#9f55c2",
9
9
  "#2a8285",
10
10
  "#697694",
11
- "#B0549d"
11
+ "#b0549d",
12
+ "#6d7c4d",
13
+ "#bd5558",
14
+ "#7665cf",
15
+ "#2d8543",
16
+ "#877410",
17
+ "#bd5a13",
18
+ "#946694",
19
+ "#008094",
20
+ "#72757a",
21
+ "#c24795",
22
+ "#667d15",
23
+ "#996a45",
24
+ "#636ebf",
25
+ "#23855e"
12
26
  ];
13
27
  const combineConfig = (config, overrides)=>({
14
28
  ...config,
@@ -47,14 +61,14 @@ const useChartOptions = ({ categoryColumnName, config, dataExclusions, dataSourc
47
61
  cancelAnimationFrame(frameRef.current);
48
62
  frameRef.current = void 0;
49
63
  }
50
- chartSeries.detach();
64
+ const { dataSource } = chartSeries;
65
+ const replacement = chartSeriesRef.current.dataSource;
66
+ const escalateToDisable = replacement !== dataSource && replacement.isSessionDataSourceOf?.(dataSource) ? false : void 0;
67
+ chartSeries.release(escalateToDisable);
51
68
  };
52
69
  }, [
53
70
  chartSeries
54
71
  ]);
55
- useEffect(()=>()=>{
56
- chartSeriesRef.current.dispose();
57
- }, []);
58
72
  useMemo(()=>{
59
73
  if (categoryColumnName !== chartSeries.categoryColumn) chartSeries.categoryColumn = categoryColumnName;
60
74
  if (itemsChanged(seriesColumnNames, chartSeries.seriesColumnNames)) chartSeries.seriesColumnNames = seriesColumnNames;
@@ -97,6 +111,9 @@ const useChartOptions = ({ categoryColumnName, config, dataExclusions, dataSourc
97
111
  trigger: "axis"
98
112
  } : void 0,
99
113
  xAxis: {
114
+ axisPointer: {
115
+ triggerEmphasis: false
116
+ },
100
117
  data: chartSeries.categories
101
118
  },
102
119
  yAxis: {
package/types/Chart.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { type MeasuredContainerProps } from "@vuu-ui/vuu-ui-controls";
2
2
  import { type ChartOptionsProps } from "./useChartOptions";
3
3
  type OptionSettings = {
4
- notMerge: boolean;
4
+ notMerge?: boolean;
5
5
  };
6
6
  type ChartSettings = {
7
7
  /**
@@ -1,7 +1,7 @@
1
- import { DataSource, DataSourceRow, DataSourceSubscribeCallback } from "@vuu-ui/vuu-data-types";
1
+ import type { DataSource, DataSourceRow, DataSourceSubscribeCallback } from "@vuu-ui/vuu-data-types";
2
2
  import { EventEmitter } from "@vuu-ui/vuu-utils";
3
- import { Series } from "./chart-types";
4
- import { ChartConfig, DataExclusionOptions } from "./useChartOptions";
3
+ import { type Series } from "./chart-types";
4
+ import type { ChartConfig, DataExclusionOptions } from "./useChartOptions";
5
5
  export type DataSourceValue = {
6
6
  key: string;
7
7
  readonly row: DataSourceRow;
@@ -21,26 +21,41 @@ export declare class ChartSeries extends EventEmitter<ChartEvents> {
21
21
  handleData: DataSourceSubscribeCallback;
22
22
  buildCategoriesAndSeries(): void;
23
23
  constructor({ config, dataExclusions, dataSource, palette, }: ChartSeriesConstructorProps);
24
+ get dataSource(): DataSource;
24
25
  /**
25
- * Subscribes to the DataSource. Invoked by the constructor, only needs to
26
- * be invoked again after dispose.
26
+ * Starts receiving data from the DataSource. A new DataSource is
27
+ * subscribed. A DataSource already in use, e.g. one suspended by a Table
28
+ * showing the same data, is resumed with the chart's range. The previous
29
+ * range is restored by release.
27
30
  */
28
31
  subscribe(): void;
29
32
  /**
30
- * Stops processing data from the DataSource, without unsubscribing it.
31
- * Used when the DataSource is replaced, the caller may still be using
32
- * the previous DataSource (e.g. as the source of an edit session).
33
+ * Stops processing data and suspends the DataSource, restoring the range
34
+ * it had when the chart took it over. The DataSource is never
35
+ * unsubscribed, the caller owns it. Unsubscribing a session DataSource
36
+ * would end its edit session.
37
+ * @param escalateToDisable pass false when another component will resume
38
+ * the DataSource, so the suspend is not promoted to a disable.
33
39
  */
34
- detach(): void;
35
- /**
36
- * Stops processing data and unsubscribes from the DataSource.
37
- */
38
- dispose(): void;
40
+ release(escalateToDisable?: boolean): void;
39
41
  get palette(): (string | undefined)[];
40
42
  get categoryColumn(): string;
41
43
  set categoryColumn(categoryColumn: string);
42
44
  get seriesColumnNames(): string[];
43
45
  set seriesColumnNames(seriesColumnNames: string[]);
46
+ /**
47
+ * Assigns a colour to each series column that does not yet have one.
48
+ * Colours are taken from a runtime palette with one slot per charted
49
+ * column, filled from the palette (repeating if there are more columns
50
+ * than palette colours). A new column takes the slot matching its
51
+ * position in the charted columns. When the user switches between sets
52
+ * of columns, e.g. alpha_set_a, beta_set_a and alpha_set_b, beta_set_b,
53
+ * the nth column of each set gets the same colour, whatever the number
54
+ * of columns. Colour assignments are retained, so a column keeps its
55
+ * colour when it is charted again. If that colour is already used by
56
+ * another charted series, the next unused slot is taken.
57
+ */
58
+ private assignColors;
44
59
  get categories(): string[];
45
60
  get series(): Series[];
46
61
  private getCategoriesAndSeries;
@@ -29,6 +29,9 @@ export interface SeriesBase {
29
29
  }
30
30
  export interface LineSeries extends SeriesBase {
31
31
  connectNulls?: boolean;
32
+ emphasis?: {
33
+ disabled: boolean;
34
+ };
32
35
  lineStyle: {
33
36
  color?: string;
34
37
  width: number;
@@ -1,6 +1,6 @@
1
- import { DataSourceRow } from "@vuu-ui/vuu-data-types";
2
- import type { ECElementEvent } from "echarts/core";
3
- import { ColumnMap } from "@vuu-ui/vuu-utils";
1
+ import type { DataSourceRow } from "@vuu-ui/vuu-data-types";
2
+ import type { ECElementEvent, ElementEvent } from "echarts/core";
3
+ import type { ColumnMap } from "@vuu-ui/vuu-utils";
4
4
  export type ChartMenuLocation = "canvas" | "series";
5
5
  export type ChartContextMenuOptions = {
6
6
  column: string;
@@ -12,5 +12,8 @@ interface ChartContextMenuHookProps {
12
12
  categoryColumnName?: string;
13
13
  columnMap: ColumnMap;
14
14
  }
15
- export declare const useChartContextMenu: ({ columnMap, }: ChartContextMenuHookProps) => ({ data, event, seriesName }: ECElementEvent) => void;
15
+ export declare const useChartContextMenu: ({ columnMap, }: ChartContextMenuHookProps) => {
16
+ onContextMenu: ({ data, event, seriesName }: ECElementEvent) => void;
17
+ onMouseMove: ({ event }: ElementEvent) => void;
18
+ };
16
19
  export {};
@@ -1,6 +1,6 @@
1
- import { DataSource, DataSourceRow } from "@vuu-ui/vuu-data-types";
2
- import { ColumnMap } from "@vuu-ui/vuu-utils";
3
- import { type EChartsCoreOption } from "echarts/core";
1
+ import type { DataSource, DataSourceRow } from "@vuu-ui/vuu-data-types";
2
+ import { type ColumnMap } from "@vuu-ui/vuu-utils";
3
+ import type { EChartsCoreOption } from "echarts/core";
4
4
  import type { ChartSelectionModel, ItemColorFunction, SymbolSize, SymbolSizeFunction, SymbolType } from "./chart-types";
5
5
  export interface ChartConfig {
6
6
  itemColorFunction?: ItemColorFunction;