@visuallyjs/browser-ui-svelte 1.2.6 → 1.2.7

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.
@@ -1,51 +1,71 @@
1
1
  <script lang="ts">
2
+ import { PieChart, type BrowserElement, type PieChartOptions } from "@visuallyjs/browser-ui";
3
+ import type { PieChartComponentProps } from "./charts/definitions";
4
+ import { useVisuallyJsModel } from "./surface-store";
2
5
 
3
- import { onMount } from "svelte";
6
+ const { options, className = "", data, url, dataSourceFilter }: PieChartComponentProps = $props();
4
7
 
5
- import {PieChart, type BrowserElement, type PieChartOptions} from "@visuallyjs/browser-ui"
6
- import {type PieChartComponentProps} from "./charts/definitions";
7
- import {useVisuallyJsModel} from "./surface-store";
8
+ let container: BrowserElement;
9
+ let chart: PieChart | undefined = $state();
8
10
 
9
- let container:BrowserElement;
10
-
11
- let {options, className = "", data, url, dataSourceFilter }:PieChartComponentProps = $props();
12
-
13
- let chart:PieChart;
14
-
15
- export function getChart() {
16
- return chart;
11
+ export function getChart() {
12
+ return chart;
17
13
  }
18
14
 
15
+ // A single, unified effect to manage the chart's entire lifecycle.
16
+ // This effect re-runs when props that require recreation, like `options`, change.
19
17
  $effect(() => {
20
- if (chart && dataSourceFilter != null) {
21
- chart.setDataSourceFilter(dataSourceFilter)
22
- }
23
- })
18
+ if (!container) return;
24
19
 
25
- function init(opts:PieChartOptions) {
26
- chart = new PieChart(container, opts)
20
+ let chartInstance: PieChart;
21
+ const chartOpts: PieChartOptions = { ...options, dataSourceFilter };
22
+ let isDestroyed = false;
27
23
 
28
- if (url != null) {
29
- chart.load({url});
30
- } else if (data != null) {
31
- chart.load({data});
24
+ // Initialize the chart. This can be async if we need to fetch a model.
25
+ if (url == null && data == null) {
26
+ useVisuallyJsModel().then(m => {
27
+ if (!isDestroyed) { // Ensure component hasn't been destroyed in the meantime
28
+ chartOpts.dataSource = m;
29
+ chartInstance = new PieChart(container, chartOpts);
30
+ chart = chartInstance;
31
+ }
32
+ });
33
+ } else {
34
+ // Synchronous initialization with `data` or `url` props.
35
+ chartInstance = new PieChart(container, chartOpts);
36
+ chart = chartInstance;
32
37
  }
33
- }
34
38
 
35
- onMount(async () => {
39
+ // This nested effect runs ONLY when the `data` prop changes.
40
+ // It loads the new data into the existing chart without recreating it.
41
+ $effect(() => {
42
+ if (chartInstance && data != null) {
43
+ chartInstance.load({ data });
44
+ }
45
+ });
36
46
 
37
- const opts:PieChartOptions = Object.assign({dataSourceFilter}, options)
47
+ // This nested effect handles updates to the `url` prop.
48
+ $effect(() => {
49
+ if (chartInstance && url != null) {
50
+ chartInstance.load({ url });
51
+ }
52
+ });
38
53
 
39
- if (url == null && data == null) {
40
- useVisuallyJsModel().then(m => {
41
- opts.dataSource = m
42
- init(opts)
43
- })
44
- } else {
45
- init(opts)
46
- }
47
-
48
- })
54
+ // This nested effect handles updates to the filter.
55
+ $effect(() => {
56
+ if (chartInstance && dataSourceFilter != null) {
57
+ chartInstance.setDataSourceFilter(dataSourceFilter);
58
+ }
59
+ });
60
+
61
+ // The cleanup function for the main effect. It runs when the component
62
+ // is unmounted or when `options` change, ensuring the old chart is destroyed.
63
+ return () => {
64
+ isDestroyed = true;
65
+ chartInstance?.destroy();
66
+ chart = undefined;
67
+ };
68
+ });
49
69
 
50
70
  </script>
51
71
 
@@ -1,7 +1,5 @@
1
1
  <script lang="ts">
2
2
 
3
- import { onMount } from "svelte";
4
-
5
3
  import {
6
4
  SankeyChart,
7
5
  type BrowserElement,
@@ -16,49 +14,79 @@
16
14
 
17
15
  let chart:SankeyChart|null = $state(null);
18
16
 
19
- $effect(() => {
20
- if (pivot != null && chart != null) {
21
- chart.pivot(pivot as string)
22
- }
23
- })
24
-
25
17
  export function getChart() {
26
18
  return chart;
27
19
  }
28
20
 
29
- function init(opts:SankeyOptions) {
30
-
31
- chart = new SankeyChart(container, opts)
32
-
33
- if (url != null) {
34
- chart.load({url});
35
- } else if (csvData != null) {
36
- chart.load({csvData});
37
- } else if (jsonData != null) {
38
- chart.load({jsonData})
39
- }
40
- }
21
+ $effect(() => {
22
+ if (!container) return;
41
23
 
24
+ let chartInstance: SankeyChart;
25
+ const _options: SankeyOptions = { ...options };
42
26
 
43
- onMount(async () => {
44
- const _options:SankeyOptions = Object.assign({}, options)
45
27
  if (interactive != null) {
46
- _options.interactive = interactive
28
+ _options.interactive = interactive;
47
29
  }
48
30
  if (pivot != null) {
49
- _options.pivot = pivot
31
+ _options.pivot = pivot;
50
32
  }
51
33
 
34
+ let isDestroyed = false;
35
+
36
+ const setupChart = () => {
37
+ chartInstance = new SankeyChart(container, _options);
38
+ chart = chartInstance;
39
+
40
+ if (url != null) {
41
+ chartInstance.load({url});
42
+ } else if (csvData != null) {
43
+ chartInstance.load({csvData});
44
+ } else if (jsonData != null) {
45
+ chartInstance.load({jsonData})
46
+ }
47
+ };
48
+
52
49
  if (url == null && csvData == null && jsonData == null) {
53
50
  useVisuallyJsModel().then(m => {
54
- _options.dataSource = m
55
- init(_options)
56
- })
51
+ if (!isDestroyed) {
52
+ _options.dataSource = m;
53
+ setupChart();
54
+ }
55
+ });
57
56
  } else {
58
- init(_options)
57
+ setupChart();
59
58
  }
60
59
 
61
- })
60
+ $effect(() => {
61
+ if (chartInstance && pivot != null) {
62
+ chartInstance.pivot(pivot as string);
63
+ }
64
+ });
65
+
66
+ $effect(() => {
67
+ if (chartInstance && url != null) {
68
+ chartInstance.load({ url });
69
+ }
70
+ });
71
+
72
+ $effect(() => {
73
+ if (chartInstance && csvData != null) {
74
+ chartInstance.load({ csvData });
75
+ }
76
+ });
77
+
78
+ $effect(() => {
79
+ if (chartInstance && jsonData != null) {
80
+ chartInstance.load({ jsonData });
81
+ }
82
+ });
83
+
84
+ return () => {
85
+ isDestroyed = true;
86
+ chartInstance?.destroy();
87
+ chart = null;
88
+ };
89
+ });
62
90
 
63
91
  </script>
64
92
 
@@ -1,49 +1,71 @@
1
1
  <script lang="ts">
2
+ import { ScatterChart, type BrowserElement, type ScatterChartOptions } from "@visuallyjs/browser-ui";
3
+ import type { ScatterChartComponentProps } from "./charts/definitions";
4
+ import { useVisuallyJsModel } from "./surface-store";
2
5
 
3
- import { onMount } from "svelte";
6
+ const { options, className = "", data, url, dataSourceFilter }: ScatterChartComponentProps = $props();
4
7
 
5
- import {type BrowserElement, type ScatterChartOptions, ScatterChart} from "@visuallyjs/browser-ui"
6
- import {type ScatterChartComponentProps} from "./charts/definitions";
7
- import {useVisuallyJsModel} from "./surface-store";
8
+ let container: BrowserElement;
9
+ let chart: ScatterChart | undefined = $state();
8
10
 
9
- let container:BrowserElement;
10
-
11
- // ScatterChartOptions
12
- let {options, className = "", data, url, dataSourceFilter }:ScatterChartComponentProps = $props();
13
-
14
- let chart:ScatterChart;
15
-
16
- export function getChart() {
17
- return chart;
11
+ export function getChart() {
12
+ return chart;
18
13
  }
19
14
 
15
+ // A single, unified effect to manage the chart's entire lifecycle.
16
+ // This effect re-runs when props that require recreation, like `options`, change.
20
17
  $effect(() => {
21
- if (chart && dataSourceFilter != null) {
22
- chart.setDataSourceFilter(dataSourceFilter)
23
- }
24
- })
25
-
26
- function init(opts:ScatterChartOptions) {
27
- chart = new ScatterChart(container, opts)
28
- if (url != null) {
29
- chart.load({url});
30
- } else if (data != null) {
31
- chart.load({data});
32
- }
33
- }
18
+ if (!container) return;
19
+
20
+ let chartInstance: ScatterChart;
21
+ const chartOpts: ScatterChartOptions = { ...options, dataSourceFilter };
22
+ let isDestroyed = false;
34
23
 
35
- onMount(async () => {
36
- const opts:ScatterChartOptions = Object.assign({dataSourceFilter}, options)
24
+ // Initialize the chart. This can be async if we need to fetch a model.
37
25
  if (url == null && data == null) {
38
26
  useVisuallyJsModel().then(m => {
39
- opts.dataSource = m
40
- init(opts)
41
- })
27
+ if (!isDestroyed) { // Ensure component hasn't been destroyed in the meantime
28
+ chartOpts.dataSource = m;
29
+ chartInstance = new ScatterChart(container, chartOpts);
30
+ chart = chartInstance;
31
+ }
32
+ });
42
33
  } else {
43
- init(opts)
34
+ // Synchronous initialization with `data` or `url` props.
35
+ chartInstance = new ScatterChart(container, chartOpts);
36
+ chart = chartInstance;
44
37
  }
45
- })
46
38
 
39
+ // This nested effect runs ONLY when the `data` prop changes.
40
+ // It loads the new data into the existing chart without recreating it.
41
+ $effect(() => {
42
+ if (chartInstance && data != null) {
43
+ chartInstance.load({ data });
44
+ }
45
+ });
46
+
47
+ // This nested effect handles updates to the `url` prop.
48
+ $effect(() => {
49
+ if (chartInstance && url != null) {
50
+ chartInstance.load({ url });
51
+ }
52
+ });
53
+
54
+ // This nested effect handles updates to the filter.
55
+ $effect(() => {
56
+ if (chartInstance && dataSourceFilter != null) {
57
+ chartInstance.setDataSourceFilter(dataSourceFilter);
58
+ }
59
+ });
60
+
61
+ // The cleanup function for the main effect. It runs when the component
62
+ // is unmounted or when `options` change, ensuring the old chart is destroyed.
63
+ return () => {
64
+ isDestroyed = true;
65
+ chartInstance?.destroy();
66
+ chart = undefined;
67
+ };
68
+ });
47
69
 
48
70
  </script>
49
71
 
@@ -0,0 +1,153 @@
1
+ <script lang="ts">
2
+ import {
3
+ isNode,
4
+ isString,
5
+ type ShapePropertyValueDefinition,
6
+ type Vertex,
7
+ type Inspector,
8
+ log
9
+ } from "@visuallyjs/browser-ui";
10
+ import {onMount} from "svelte";
11
+ import type {ShapePropertiesInspectorProps} from "./definitions";
12
+ import {getInspectorContext} from "./inspector-store";
13
+
14
+ const MAX_RADIOS = 7;
15
+ const TRUE_FALSE = ["True", "False"]
16
+
17
+ let {
18
+ vertex,
19
+ maxRadios = MAX_RADIOS
20
+ }: ShapePropertiesInspectorProps = $props()
21
+
22
+ let inspector: Inspector | null = $state(null)
23
+ let shapeDef = $state(null)
24
+
25
+ function getBooleanLabel(prop: any, value: boolean) {
26
+ const labels = prop.booleanLabels || TRUE_FALSE
27
+ return labels[value ? 0 : 1]
28
+ }
29
+
30
+ function getShapePropertyValueLabel(v: string | ShapePropertyValueDefinition): string {
31
+ return isString(v) ? v : (v as ShapePropertyValueDefinition).label
32
+ }
33
+
34
+ function getShapePropertyValueValue(v: string | ShapePropertyValueDefinition): string {
35
+ return isString(v) ? v : (v as ShapePropertyValueDefinition).value
36
+ }
37
+
38
+ function resolveShapeDef(insp: Inspector, v: Vertex) {
39
+ if (!insp || !v || !isNode(v)) return null
40
+ const {type, category} = v.data;
41
+ if (!type || !category) return null;
42
+
43
+ const shapeLibrary = insp.$ui.getShapeLibrary();
44
+ const shapeSet = shapeLibrary.getShapeSet(category);
45
+ if (!shapeSet) return null;
46
+ return shapeSet.shapes.find(s => s.type === type)
47
+ }
48
+
49
+ onMount(() => {
50
+ const ic = getInspectorContext(true)
51
+ if (ic == null) {
52
+ log(`WARN: ShapePropertiesInspector cannot find an inspector context; cannot mount.`)
53
+ } else {
54
+ ic.listen(i => {
55
+ inspector = i
56
+ })
57
+ }
58
+ })
59
+
60
+ $effect(() => {
61
+ if (inspector && vertex) {
62
+ shapeDef = resolveShapeDef(inspector, vertex)
63
+ } else {
64
+ shapeDef = null
65
+ }
66
+ })
67
+
68
+ </script>
69
+
70
+ {#if shapeDef && shapeDef.properties}
71
+ {#each shapeDef.properties as prop (prop.id)}
72
+ {@const isBoolean = prop.type === 'boolean'}
73
+ {@const hasValues = prop.values && prop.values.length > 0}
74
+ {@const useRadio = hasValues && prop.editor !== 'select' && prop.values.length <= maxRadios}
75
+
76
+ <div class="vjs-inspector-field">
77
+ {#if !isBoolean || prop.editor === 'select' || prop.editor === 'radio'}
78
+ <label class="vjs-inspector-property-label">{prop.label || prop.id}</label>
79
+ {/if}
80
+
81
+ {#if prop.type === 'string'}
82
+ {#if hasValues}
83
+ {#if useRadio}
84
+ <div class="vjs-radio-group">
85
+ {#each prop.values as v (getShapePropertyValueLabel(v))}
86
+ <label>
87
+ <input type="radio" name={prop.id} vjs-att={prop.id} value={getShapePropertyValueValue(v)} /> {getShapePropertyValueLabel(v)}
88
+ </label>
89
+ {/each}
90
+ </div>
91
+ {:else}
92
+ <select vjs-att={prop.id}>
93
+ {#each prop.values as v (getShapePropertyValueLabel(v))}
94
+ <option value={getShapePropertyValueValue(v)}>{getShapePropertyValueLabel(v)}</option>
95
+ {/each}
96
+ </select>
97
+ {/if}
98
+ {:else}
99
+ <input type="text" vjs-att={prop.id} placeholder={prop.description} />
100
+ {/if}
101
+ {:else}
102
+ {#if prop.type === 'number'}
103
+ {#if hasValues}
104
+ {#if useRadio}
105
+ <div class="vjs-radio-group">
106
+ {#each prop.values as v (getShapePropertyValueLabel(v))}
107
+ <label>
108
+ <input type="radio" vjs-att={prop.id} value={getShapePropertyValueValue(v)} /> {getShapePropertyValueLabel(v)}
109
+ </label>
110
+ {/each}
111
+ </div>
112
+ {:else}
113
+ <select vjs-att={prop.id}>
114
+ {#each prop.values as v (getShapePropertyValueLabel(v))}
115
+ <option value={getShapePropertyValueValue(v)}>{getShapePropertyValueLabel(v)}</option>
116
+ {/each}
117
+ </select>
118
+ {/if}
119
+ {:else}
120
+ <input type="number" vjs-att={prop.id} placeholder={prop.description} min={prop.min} max={prop.max} />
121
+ {/if}
122
+ {:else}
123
+ {#if isBoolean}
124
+ {#if prop.editor === 'select'}
125
+ <select vjs-att={prop.id} vjs-datatype="boolean">
126
+ <option value=""></option>
127
+ <option value="true">{getBooleanLabel(prop, true)}</option>
128
+ <option value="false">{getBooleanLabel(prop, false)}</option>
129
+ </select>
130
+ {:else}
131
+ {#if prop.editor === 'radio'}
132
+ <div class="vjs-radio-group">
133
+ <label><input type="radio" name={prop.id} vjs-att={prop.id} vjs-datatype="boolean" value="true" /><span>{getBooleanLabel(prop, true)}</span></label>
134
+ <label><input type="radio" name={prop.id} vjs-att={prop.id} vjs-datatype="boolean" value="false" /><span>{getBooleanLabel(prop, false)}</span></label>
135
+ </div>
136
+ {:else}
137
+ <label>
138
+ <input type="checkbox" vjs-datatype="boolean" vjs-att={prop.id} /> {prop.label || prop.id}
139
+ </label>
140
+ {/if}
141
+ {/if}
142
+ {:else}
143
+ <input type="text" vjs-att={prop.id} placeholder={prop.description} />
144
+ {/if}
145
+ {/if}
146
+ {/if}
147
+
148
+ {#if prop.description}
149
+ <div class="vjs-field-desc">{prop.description}</div>
150
+ {/if}
151
+ </div>
152
+ {/each}
153
+ {/if}
@@ -1,52 +1,71 @@
1
1
  <script lang="ts">
2
+ import { CategoryValueChart, type BrowserElement, type CategoryValueChartOptions } from "@visuallyjs/browser-ui";
3
+ import type { XYChartComponentProps } from "./charts/definitions";
4
+ import { useVisuallyJsModel } from "./surface-store";
2
5
 
3
- import { onMount } from "svelte";
6
+ const { options, className = "", data, url, dataSourceFilter }: XYChartComponentProps = $props();
4
7
 
5
- import {
6
- CategoryValueChart,
7
- type CategoryValueChartOptions
8
- } from "@visuallyjs/browser-ui"
9
- import {type XYChartComponentProps} from "./charts/definitions";
10
- import {useVisuallyJsModel} from "./surface-store";
8
+ let container: BrowserElement;
9
+ let chart: CategoryValueChart | undefined = $state();
11
10
 
12
- let container:HTMLElement;
13
-
14
- // XYChartOptions
15
- let {options, className = "", data, url, dataSourceFilter }:XYChartComponentProps = $props()
16
-
17
- let chart:CategoryValueChart
18
-
19
- export function getChart() {
20
- return chart;
21
- }
22
-
23
- function init(opts:CategoryValueChartOptions) {
24
- chart = new CategoryValueChart(container, opts)
25
- if (url != null) {
26
- chart.load({url});
27
- } else if (data != null) {
28
- chart.load({data});
29
- }
11
+ export function getChart() {
12
+ return chart;
30
13
  }
31
14
 
15
+ // A single, unified effect to manage the chart's entire lifecycle.
16
+ // This effect re-runs when props that require recreation, like `options`, change.
32
17
  $effect(() => {
33
- if (chart && dataSourceFilter != null) {
34
- chart.setDataSourceFilter(dataSourceFilter)
35
- }
36
- })
18
+ if (!container) return;
19
+
20
+ let chartInstance: CategoryValueChart;
21
+ const chartOpts: CategoryValueChartOptions = { ...options, dataSourceFilter };
22
+ let isDestroyed = false;
37
23
 
38
- onMount(async () => {
39
- const opts:CategoryValueChartOptions = Object.assign({}, options)
24
+ // Initialize the chart. This can be async if we need to fetch a model.
40
25
  if (url == null && data == null) {
41
26
  useVisuallyJsModel().then(m => {
42
- opts.dataSource = m
43
- init(opts)
44
- })
27
+ if (!isDestroyed) { // Ensure component hasn't been destroyed in the meantime
28
+ chartOpts.dataSource = m;
29
+ chartInstance = new CategoryValueChart(container, chartOpts);
30
+ chart = chartInstance;
31
+ }
32
+ });
45
33
  } else {
46
- init(opts)
34
+ // Synchronous initialization with `data` or `url` props.
35
+ chartInstance = new CategoryValueChart(container, chartOpts);
36
+ chart = chartInstance;
47
37
  }
48
- })
49
38
 
39
+ // This nested effect runs ONLY when the `data` prop changes.
40
+ // It loads the new data into the existing chart without recreating it.
41
+ $effect(() => {
42
+ if (chartInstance && data != null) {
43
+ chartInstance.load({ data });
44
+ }
45
+ });
46
+
47
+ // This nested effect handles updates to the `url` prop.
48
+ $effect(() => {
49
+ if (chartInstance && url != null) {
50
+ chartInstance.load({ url });
51
+ }
52
+ });
53
+
54
+ // This nested effect handles updates to the filter.
55
+ $effect(() => {
56
+ if (chartInstance && dataSourceFilter != null) {
57
+ chartInstance.setDataSourceFilter(dataSourceFilter);
58
+ }
59
+ });
60
+
61
+ // The cleanup function for the main effect. It runs when the component
62
+ // is unmounted or when `options` change, ensuring the old chart is destroyed.
63
+ return () => {
64
+ isDestroyed = true;
65
+ chartInstance?.destroy();
66
+ chart = undefined;
67
+ };
68
+ });
50
69
 
51
70
  </script>
52
71
 
package/components.d.ts CHANGED
@@ -14,6 +14,8 @@ export { default as ShapePaletteComponent } from "./ShapePaletteComponent.svelte
14
14
  export { default as DiagramPaletteComponent } from "./DiagramPaletteComponent.svelte";
15
15
  export { default as ShapeComponent } from "./ShapeComponent.svelte";
16
16
  export { default as EdgeTypePickerComponent } from "./EdgeTypePickerComponent.svelte";
17
+ export { default as EdgePropertyMappingsInspector } from "./EdgePropertyMappingsInspector.svelte";
18
+ export { default as ShapePropertiesInspector } from "./ShapePropertiesInspector.svelte";
17
19
  export { default as ColorPickerComponent } from "./ColorPickerComponent.svelte";
18
20
  export { default as SurfacePopup } from "./SurfacePopup.svelte";
19
21
  export { default as ColumnChartComponent } from "./ColumnChartComponent.svelte";
package/components.js CHANGED
@@ -14,6 +14,8 @@ export { default as ShapePaletteComponent } from "./ShapePaletteComponent.svelte
14
14
  export { default as DiagramPaletteComponent } from "./DiagramPaletteComponent.svelte";
15
15
  export { default as ShapeComponent } from "./ShapeComponent.svelte";
16
16
  export { default as EdgeTypePickerComponent } from "./EdgeTypePickerComponent.svelte";
17
+ export { default as EdgePropertyMappingsInspector } from "./EdgePropertyMappingsInspector.svelte";
18
+ export { default as ShapePropertiesInspector } from "./ShapePropertiesInspector.svelte";
17
19
  export { default as ColorPickerComponent } from "./ColorPickerComponent.svelte";
18
20
  export { default as SurfacePopup } from "./SurfacePopup.svelte";
19
21
  export { default as ColumnChartComponent } from "./ColumnChartComponent.svelte";