From 4be9d7ee2e995d46839924e9355917297202b8ad Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:02:14 +0100 Subject: [PATCH 01/18] Add Skeleton.tsx --- .../packages/lib/src/plugins/mui/Skeleton.tsx | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx diff --git a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx new file mode 100644 index 00000000..7fb5ac8e --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx @@ -0,0 +1,37 @@ +import { + Skeleton as MuiSkeleton, + type SkeletonProps as MuiSkeletonProps, +} from "@mui/material"; + +import type { ComponentState } from "@/index"; +import type { ReactElement } from "react"; + +interface SkeletonState extends Omit { + variant?: MuiSkeletonProps["variant"]; + width?: MuiSkeletonProps["width"]; + height?: MuiSkeletonProps["height"]; + animation?: MuiSkeletonProps["animation"]; + loading: boolean; + children?: ReactElement; +} + +interface SkeletonProps extends SkeletonState {} + +export const Skeleton = ({ + id, + style, + loading, + children, + ...skeletonProps +}: SkeletonProps) => { + return loading && skeletonProps && Object.keys(skeletonProps).length > 0 ? ( + + ) : ( + children + ); +}; From f84c74343dceb620ffb4917b85a8042ad6924e29 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:02:31 +0100 Subject: [PATCH 02/18] Add Skeleton.test.tsx --- .../lib/src/plugins/mui/Skeleton.test.tsx | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx diff --git a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx new file mode 100644 index 00000000..bac05717 --- /dev/null +++ b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx @@ -0,0 +1,54 @@ +import { describe, expect, it } from "vitest"; +import { render, screen } from "@testing-library/react"; +import { Skeleton } from "@/plugins/mui/Skeleton"; + +describe("Skeleton", () => { + it("should render the MUI Skeleton when loading is true", () => { + render(); + const muiSkeleton = screen.getByTestId("skeleton-test-id"); + expect(muiSkeleton).toBeInTheDocument(); + expect(muiSkeleton).toHaveClass("MuiSkeleton-root"); + }); + + it("should not render the MUI Skeleton without skeletonProps", () => { + render(); + const muiSkeleton = screen.queryByTestId("skeleton-test-id"); + expect(muiSkeleton).not.toBeInTheDocument(); + }); + + it("should not render the MUI Skeleton and render children when loading is false", () => { + render( + +
Test Content
+
, + ); + const muiSkeleton = screen.queryByTestId("skeleton-test-id"); + expect(muiSkeleton).not.toBeInTheDocument(); + expect(screen.getByText("Test Content")).toBeInTheDocument(); + }); + + it("should render with the specified variant", () => { + render(); + const muiSkeleton = screen.getByTestId("skeleton-test-id"); + expect(muiSkeleton).toHaveClass("MuiSkeleton-circular"); + }); + + it("should render with specified width and height", () => { + render(); + const muiSkeleton = screen.getByTestId("skeleton-test-id"); + expect(muiSkeleton).toHaveStyle("width: 100px"); + expect(muiSkeleton).toHaveStyle("height: 50px"); + }); + + it("should render with specified style", () => { + render(); + const muiSkeleton = screen.getByTestId("skeleton-test-id"); + expect(muiSkeleton).toHaveStyle("background-color: rgb(255, 0, 0);"); + }); + + it("should render with specified animation", () => { + render(); + const muiSkeleton = screen.getByTestId("skeleton-test-id"); + expect(muiSkeleton).toHaveClass("MuiSkeleton-wave"); + }); +}); From ee4fb76d008c5f7a66d48c18fd36793d8f04a2dc Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:03:14 +0100 Subject: [PATCH 03/18] update component.ts and vegaCharts to include Skeleton --- .../lib/src/plugins/vega/VegaChart.tsx | 47 ++++++++++++------- .../packages/lib/src/types/state/component.ts | 2 + 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx index 69ffa09b..7be73337 100644 --- a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx +++ b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx @@ -1,9 +1,11 @@ import { VegaLite } from "react-vega"; import type { TopLevelSpec } from "vega-lite"; -import type { ComponentState, ComponentProps } from "@/index"; +import type { ComponentProps, ComponentState } from "@/index"; import { useSignalListeners } from "./hooks/useSignalListeners"; import { useVegaTheme, type VegaTheme } from "./hooks/useVegaTheme"; +import { useEffect, useState } from "react"; +import { Skeleton } from "@/plugins/mui/Skeleton"; interface VegaChartState extends ComponentState { theme?: VegaTheme | "default" | "system"; @@ -19,22 +21,35 @@ export function VegaChart({ id, style, theme, - chart, + chart: initialChart, + skeletonProps, onChange, }: VegaChartProps) { - const signalListeners = useSignalListeners(chart, type, id, onChange); + const signalListeners = useSignalListeners(initialChart, type, id, onChange); const vegaTheme = useVegaTheme(theme); - if (chart) { - return ( - - ); - } else { - return
; - } + const [chart, setChart] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + if (initialChart) { + setLoading(false); + setChart(initialChart); + } + }, [initialChart]); + + return ( + + {chart ? ( + + ) : ( +
+ )} + + ); } diff --git a/chartlets.js/packages/lib/src/types/state/component.ts b/chartlets.js/packages/lib/src/types/state/component.ts index 5bb5033c..5827e47d 100644 --- a/chartlets.js/packages/lib/src/types/state/component.ts +++ b/chartlets.js/packages/lib/src/types/state/component.ts @@ -1,6 +1,7 @@ import { type CSSProperties } from "react"; import { isObject } from "@/utils/isObject"; import { isString } from "@/utils/isString"; +import type { SkeletonProps } from "@mui/material"; export type ComponentNode = | ComponentState @@ -24,6 +25,7 @@ export interface ComponentState { label?: string; color?: string; tooltip?: string; + skeletonProps?: SkeletonProps; } export interface ContainerState extends ComponentState { From 16be4289897d804322fe0c6af649866fbf27a510 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:07:23 +0100 Subject: [PATCH 04/18] Add skeleton.py in chartlets.py --- chartlets.py/chartlets/components/__init__.py | 1 + .../chartlets/components/charts/vega.py | 4 +++ chartlets.py/chartlets/components/skeleton.py | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 chartlets.py/chartlets/components/skeleton.py diff --git a/chartlets.py/chartlets/components/__init__.py b/chartlets.py/chartlets/components/__init__.py index 615d95c0..ad05bdf7 100644 --- a/chartlets.py/chartlets/components/__init__.py +++ b/chartlets.py/chartlets/components/__init__.py @@ -10,6 +10,7 @@ from .radiogroup import Radio from .radiogroup import RadioGroup from .select import Select +from .skeleton import Skeleton from .slider import Slider from .switch import Switch from .tabs import Tab diff --git a/chartlets.py/chartlets/components/charts/vega.py b/chartlets.py/chartlets/components/charts/vega.py index 45e6f929..b96e5ea6 100644 --- a/chartlets.py/chartlets/components/charts/vega.py +++ b/chartlets.py/chartlets/components/charts/vega.py @@ -36,6 +36,10 @@ class VegaChart(Component): chart: altair.Chart | None = None """The [Vega Altair chart](https://altair-viz.github.io/gallery/index.html).""" + skeletonProps: dict[str, Any] | None = None + """Add the skeleton props from the Skeleton MUI component to render a + skeleton during long loading times.""" + def to_dict(self) -> dict[str, Any]: d = super().to_dict() if self.chart is not None: diff --git a/chartlets.py/chartlets/components/skeleton.py b/chartlets.py/chartlets/components/skeleton.py new file mode 100644 index 00000000..ebd59427 --- /dev/null +++ b/chartlets.py/chartlets/components/skeleton.py @@ -0,0 +1,27 @@ +from dataclasses import dataclass +from typing import Literal + +from chartlets import Component + + +@dataclass(frozen=True) +class Skeleton(Component): + """Display a placeholder preview of your content before the data gets + loaded to reduce load-time frustration.""" + + variant: Literal["text", "rectangular", "circular", "rounded"] | str | None\ + = None + """The type of skeleton to display.""" + + width: str | int | None = None + """Width of the skeleton. Can be a number (pixels) or a string (e.g., '100%').""" + + height: str | int | None = None + """Height of the skeleton. Can be a number (pixels) or a string (e.g., '50px').""" + + animation: Literal["pulse", "wave", False] | None = None + """The animation effect to use. + - 'pulse': A subtle pulsing animation. + - 'wave': A shimmering animation. + - False: No animation. + """ From 4c099b638a9932f75d776c3d7ea0baffa2c45746 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:07:37 +0100 Subject: [PATCH 05/18] add test --- .../tests/components/skeleton_test.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 chartlets.py/tests/components/skeleton_test.py diff --git a/chartlets.py/tests/components/skeleton_test.py b/chartlets.py/tests/components/skeleton_test.py new file mode 100644 index 00000000..8b6a2cb3 --- /dev/null +++ b/chartlets.py/tests/components/skeleton_test.py @@ -0,0 +1,24 @@ +from chartlets.components import Skeleton +from tests.component_test import make_base + + +class SkeletonTest(make_base(Skeleton)): + + def test_is_json_serializable(self): + self.assert_is_json_serializable( + self.cls( + width=100, + height=50, + animation="pulse", + id="my-skeleton", + variant="rounded", + ), + { + "type": "Skeleton", + "width": 100, + "height": 50, + "animation": "pulse", + "id": "my-skeleton", + "variant": "rounded", + }, + ) From ad990acf82e583d07280e48df13aeefde23a3de5 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:07:45 +0100 Subject: [PATCH 06/18] update demo --- chartlets.py/demo/my_extension/my_panel_1.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/chartlets.py/demo/my_extension/my_panel_1.py b/chartlets.py/demo/my_extension/my_panel_1.py index 2783fa32..6242c683 100644 --- a/chartlets.py/demo/my_extension/my_panel_1.py +++ b/chartlets.py/demo/my_extension/my_panel_1.py @@ -1,7 +1,7 @@ from typing import Any import altair as alt from chartlets import Component, Input, Output, State -from chartlets.components import VegaChart, Box, Select +from chartlets.components import VegaChart, Box, Select, Skeleton from server.context import Context from server.panel import Panel @@ -13,8 +13,12 @@ @panel.layout() def render_panel(ctx: Context) -> Component: selected_dataset: int = 0 + chart_skeleton = Skeleton(height="100px", variant="rounded", animation="wave") chart = VegaChart( - id="chart", chart=make_chart(ctx, selected_dataset), style={"flexGrow": 1} + id="chart", + chart=make_chart(ctx, selected_dataset), + style={"flexGrow": 1}, + skeletonProps=chart_skeleton.to_dict(), ) select = Select( id="selected_dataset", From 24deaa83c59f651b922dc582a1244050a38d5ad5 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Mon, 23 Dec 2024 17:12:34 +0100 Subject: [PATCH 07/18] Update CHANGES.md --- chartlets.js/CHANGES.md | 1 + chartlets.py/CHANGES.md | 1 + 2 files changed, 2 insertions(+) diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index 7e5fd11f..f7cd753f 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -43,6 +43,7 @@ - `Switch` - `Tabs` - `Slider` + - `Skeleton` (currently supported for Vega Charts) * Supporting `tooltip` property for interactive MUI components. diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index 6dd0fc46..b4be55a0 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -23,6 +23,7 @@ - `RadioGroup` and `Radio` - `Tabs` - `Slider` + - `Skeleton` (currently supported for Vega Charts) ## Version 0.0.29 (from 2024/11/26) From 49d60d563157e3c591a2d72ffb9f6078f90581a6 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 14:56:51 +0100 Subject: [PATCH 08/18] all interactions leading to update in chart can be skeletonized --- .../lib/src/actions/handleComponentChange.ts | 5 ++ .../lib/src/actions/handleHostStoreChange.ts | 69 +++++++++++++++---- .../src/actions/helpers/invokeCallbacks.ts | 42 ++++++++++- chartlets.js/packages/lib/src/hooks.ts | 3 + .../packages/lib/src/plugins/mui/Skeleton.tsx | 51 ++++++++++---- .../lib/src/plugins/vega/VegaChart.tsx | 57 ++++++++------- chartlets.js/packages/lib/src/store.ts | 2 + .../packages/lib/src/types/model/callback.ts | 12 +++- .../packages/lib/src/types/state/component.ts | 4 +- .../packages/lib/src/types/state/store.ts | 6 ++ .../packages/lib/src/utils/compare.test.ts | 38 ++++++++++ .../packages/lib/src/utils/compare.ts | 12 ++++ chartlets.py/chartlets/components/skeleton.py | 8 +++ 13 files changed, 253 insertions(+), 56 deletions(-) create mode 100644 chartlets.js/packages/lib/src/utils/compare.test.ts create mode 100644 chartlets.js/packages/lib/src/utils/compare.ts diff --git a/chartlets.js/packages/lib/src/actions/handleComponentChange.ts b/chartlets.js/packages/lib/src/actions/handleComponentChange.ts index ea8528e1..183743e1 100644 --- a/chartlets.js/packages/lib/src/actions/handleComponentChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleComponentChange.ts @@ -70,6 +70,10 @@ function getCallbackRequests( equalObjPaths(input.property, changeEvent.property), ); if (inputIndex >= 0) { + // Collect output IDs for updating their respective loading states + const outputs = contribution.callbacks?.[callbackIndex]["outputs"]; + const outputIds: string[] = + outputs?.map((output) => output.id as string) ?? []; // Collect triggered callback callbackRequests.push({ contribPoint, @@ -77,6 +81,7 @@ function getCallbackRequests( callbackIndex, inputIndex, inputValues: getInputValues(inputs, contribution, hostStore), + outputIds: outputIds, }); } } diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts index 31c7c55b..4df6f04d 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChange.ts @@ -11,6 +11,7 @@ import { invokeCallbacks } from "@/actions/helpers/invokeCallbacks"; import type { ContributionState } from "@/types/state/contribution"; import type { HostStore } from "@/types/state/host"; import { store } from "@/store"; +import { shallowEqualArrays } from "@/utils/compare"; /** * A reference to a property of an input of a callback of a contribution. @@ -43,29 +44,69 @@ export function handleHostStoreChange() { contributionsRecord, hostStore, ); - if (callbackRequests && callbackRequests.length > 0) { - invokeCallbacks(callbackRequests); + + const filteredCallbackRequests = callbackRequests.filter( + (callbackRequest): callbackRequest is CallbackRequest => + callbackRequest !== undefined, + ); + if (filteredCallbackRequests && filteredCallbackRequests.length > 0) { + invokeCallbacks(filteredCallbackRequests); } } -function getCallbackRequests( +// Exporting for testing only +export function getCallbackRequests( propertyRefs: PropertyRef[], contributionsRecord: Record, hostStore: HostStore, -): CallbackRequest[] { - return propertyRefs.map((propertyRef) => { - const contributions = contributionsRecord[propertyRef.contribPoint]; - const contribution = contributions[propertyRef.contribIndex]; - const callback = contribution.callbacks![propertyRef.callbackIndex]; - const inputValues = getInputValues( - callback.inputs!, - contribution, +): (CallbackRequest | undefined)[] { + const { lastCallbackInputValues } = store.getState(); + return propertyRefs.map((propertyRef) => + getCallbackRequest( + propertyRef, + lastCallbackInputValues, + contributionsRecord, hostStore, - ); - return { ...propertyRef, inputValues }; - }); + ), + ); } +const getCallbackRequest = ( + propertyRef: PropertyRef, + lastCallbackInputValues: Record, + contributionsRecord: Record, + hostStore: HostStore, +) => { + const contribPoint: string = propertyRef.contribPoint; + const contribIndex: number = propertyRef.contribIndex; + const callbackIndex: number = propertyRef.callbackIndex; + const contributions = contributionsRecord[contribPoint]; + const contribution = contributions[contribIndex]; + const callback = contribution.callbacks![callbackIndex]; + const inputValues = getInputValues(callback.inputs!, contribution, hostStore); + const callbackId = `${contribPoint}-${contribIndex}-${callbackIndex}`; + const lastInputValues = lastCallbackInputValues[callbackId]; + if (shallowEqualArrays(lastInputValues, inputValues)) { + // We no longer log, as the situation is quite common + // Enable error logging for debugging only: + // console.groupCollapsed("Skipping callback request"); + // console.debug("inputValues", inputValues); + // console.groupEnd(); + return undefined; + } + store.setState({ + lastCallbackInputValues: { + ...lastCallbackInputValues, + [callbackId]: inputValues, + }, + }); + // Collect output IDs for updating their respective loading states + const outputs = contribution.callbacks?.[callbackIndex]["outputs"]; + const outputIds: string[] = + outputs?.map((output) => output.id as string) ?? []; + return { ...propertyRef, inputValues, outputIds }; +}; + // TODO: use a memoized selector to get hostStorePropertyRefs // Note that this will only be effective and once we split the // static contribution infos and dynamic contribution states. diff --git a/chartlets.js/packages/lib/src/actions/helpers/invokeCallbacks.ts b/chartlets.js/packages/lib/src/actions/helpers/invokeCallbacks.ts index 1fe177ee..e1c8187a 100644 --- a/chartlets.js/packages/lib/src/actions/helpers/invokeCallbacks.ts +++ b/chartlets.js/packages/lib/src/actions/helpers/invokeCallbacks.ts @@ -4,7 +4,7 @@ import { fetchCallback } from "@/api/fetchCallback"; import { applyStateChangeRequests } from "@/actions/helpers/applyStateChangeRequests"; export function invokeCallbacks(callbackRequests: CallbackRequest[]) { - const { configuration } = store.getState(); + const { configuration, loadingState } = store.getState(); const shouldLog = configuration.logging?.enabled; const invocationId = getInvocationId(); if (shouldLog) { @@ -13,6 +13,20 @@ export function invokeCallbacks(callbackRequests: CallbackRequest[]) { callbackRequests, ); } + + // Set the respective callback's outputs loading state to true before + // sending the request + callbackRequests.forEach((callbackRequest) => { + const outputIds = callbackRequest.outputIds; + outputIds.forEach((outputId) => { + store.setState({ + loadingState: { + ...loadingState, + [outputId]: true, + }, + }); + }); + }); fetchCallback(callbackRequests, configuration.api).then( (changeRequestsResult) => { if (changeRequestsResult.data) { @@ -23,6 +37,19 @@ export function invokeCallbacks(callbackRequests: CallbackRequest[]) { ); } applyStateChangeRequests(changeRequestsResult.data); + // Set the loading state of each output ID of the callback's that + // were invoked to false as the fetch was successful. + callbackRequests.forEach((callbackRequest) => { + const outputIds = callbackRequest.outputIds; + outputIds.forEach((outputId) => { + store.setState({ + loadingState: { + ...loadingState, + [outputId]: false, + }, + }); + }); + }); } else { console.error( "callback failed:", @@ -30,6 +57,19 @@ export function invokeCallbacks(callbackRequests: CallbackRequest[]) { "for call requests:", callbackRequests, ); + // Set the loading state of each output ID of the callback's that + // were invoked to `failed` as the fetch was unsuccessful. + callbackRequests.forEach((callbackRequest) => { + const outputIds = callbackRequest.outputIds; + outputIds.forEach((outputId) => { + store.setState({ + loadingState: { + ...loadingState, + [outputId]: "failed", + }, + }); + }); + }); } }, ); diff --git a/chartlets.js/packages/lib/src/hooks.ts b/chartlets.js/packages/lib/src/hooks.ts index 43882e14..4f2b8f39 100644 --- a/chartlets.js/packages/lib/src/hooks.ts +++ b/chartlets.js/packages/lib/src/hooks.ts @@ -20,6 +20,8 @@ const selectContributionsRecord = (state: StoreState) => const selectThemeMode = (state: StoreState) => state.themeMode; +const selectLoadingState = (state: StoreState) => state.loadingState; + const useStore = store; export const useConfiguration = () => useStore(selectConfiguration); @@ -27,6 +29,7 @@ export const useExtensions = () => useStore(selectExtensions); export const useContributionsResult = () => useStore(selectContributionsResult); export const useContributionsRecord = () => useStore(selectContributionsRecord); export const useThemeMode = () => useStore(selectThemeMode); +export const useLoadingState = () => useStore(selectLoadingState); /** * A hook that retrieves the contributions for the given contribution diff --git a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx index 7fb5ac8e..9269cc99 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx @@ -11,27 +11,52 @@ interface SkeletonState extends Omit { width?: MuiSkeletonProps["width"]; height?: MuiSkeletonProps["height"]; animation?: MuiSkeletonProps["animation"]; - loading: boolean; + opacity?: number; + isLoading: boolean; children?: ReactElement; } -interface SkeletonProps extends SkeletonState {} +export interface SkeletonProps extends SkeletonState {} export const Skeleton = ({ id, style, - loading, children, - ...skeletonProps + isLoading, + ...props }: SkeletonProps) => { - return loading && skeletonProps && Object.keys(skeletonProps).length > 0 ? ( - - ) : ( - children + const opacity: number = props.opacity ?? 0.7; + props.width = props.width ?? "100%"; + props.height = props.height ?? "100%"; + console.log("opacity", opacity); + console.log("props.width", props.width); + console.log("props.height", props.height); + return ( +
+ {children} + {isLoading && ( +
+ +
+ )} +
); }; diff --git a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx index 7be73337..1f6cf2f4 100644 --- a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx +++ b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx @@ -1,11 +1,12 @@ import { VegaLite } from "react-vega"; import type { TopLevelSpec } from "vega-lite"; -import type { ComponentProps, ComponentState } from "@/index"; -import { useSignalListeners } from "./hooks/useSignalListeners"; +import { type ComponentProps, type ComponentState } from "@/index"; import { useVegaTheme, type VegaTheme } from "./hooks/useVegaTheme"; -import { useEffect, useState } from "react"; +import { useSignalListeners } from "@/plugins/vega/hooks/useSignalListeners"; import { Skeleton } from "@/plugins/mui/Skeleton"; +import { useLoadingState } from "@/hooks"; +import type { ReactElement } from "react"; interface VegaChartState extends ComponentState { theme?: VegaTheme | "default" | "system"; @@ -27,29 +28,39 @@ export function VegaChart({ }: VegaChartProps) { const signalListeners = useSignalListeners(initialChart, type, id, onChange); const vegaTheme = useVegaTheme(theme); - const [chart, setChart] = useState(null); - const [loading, setLoading] = useState(true); - useEffect(() => { - if (initialChart) { - setLoading(false); - setChart(initialChart); - } - }, [initialChart]); + const loadingState = useLoadingState(); + if (!id) { + return; + } + const isLoading = loadingState[id]; + if (isLoading == "failed") { + return
An error occurred while loading the data.
; + } + const chart: ReactElement | null = initialChart ? ( + + ) : ( +
+ ); + const isSkeletonRequired = skeletonProps !== undefined; + if (!isSkeletonRequired) { + return chart; + } + const skeletonId = id + "-skeleton"; return ( - - {chart ? ( - - ) : ( -
- )} + + {chart} ); } diff --git a/chartlets.js/packages/lib/src/store.ts b/chartlets.js/packages/lib/src/store.ts index 89ca4e91..9fe699c3 100644 --- a/chartlets.js/packages/lib/src/store.ts +++ b/chartlets.js/packages/lib/src/store.ts @@ -7,4 +7,6 @@ export const store = create(() => ({ extensions: [], contributionsResult: {}, contributionsRecord: {}, + lastCallbackInputValues: {}, + loadingState: {}, })); diff --git a/chartlets.js/packages/lib/src/types/model/callback.ts b/chartlets.js/packages/lib/src/types/model/callback.ts index 12117833..789b2be4 100644 --- a/chartlets.js/packages/lib/src/types/model/callback.ts +++ b/chartlets.js/packages/lib/src/types/model/callback.ts @@ -73,8 +73,8 @@ export interface InputRef { /** * A `CallbackRequest` is a request to invoke a server-side callback. - * The result from invoking server-side callbacks is a list of `StateChangeRequest` - * instances. + * The result from invoking server-side callbacks is a list of + * `StateChangeRequest` instances. */ export interface CallbackRequest extends ContribRef, CallbackRef, InputRef { /** @@ -83,11 +83,17 @@ export interface CallbackRequest extends ContribRef, CallbackRef, InputRef { * as the callback's inputs. */ inputValues: unknown[]; + /** + * The output IDs of the callback that will be used to update the + * loading state of its respective output. + */ + outputIds: string[]; } /** * A `StateChangeRequest` is a request to change the application state. - * Instances of this interface are returned from invoking a server-side callback. + * Instances of this interface are returned from invoking a server-side + * callback. */ export interface StateChangeRequest extends ContribRef { /** diff --git a/chartlets.js/packages/lib/src/types/state/component.ts b/chartlets.js/packages/lib/src/types/state/component.ts index 5827e47d..04f00e9d 100644 --- a/chartlets.js/packages/lib/src/types/state/component.ts +++ b/chartlets.js/packages/lib/src/types/state/component.ts @@ -1,7 +1,7 @@ import { type CSSProperties } from "react"; import { isObject } from "@/utils/isObject"; import { isString } from "@/utils/isString"; -import type { SkeletonProps } from "@mui/material"; +import type { SkeletonProps } from "@/plugins/mui/Skeleton"; export type ComponentNode = | ComponentState @@ -25,7 +25,7 @@ export interface ComponentState { label?: string; color?: string; tooltip?: string; - skeletonProps?: SkeletonProps; + skeletonProps?: Omit; } export interface ContainerState extends ComponentState { diff --git a/chartlets.js/packages/lib/src/types/state/store.ts b/chartlets.js/packages/lib/src/types/state/store.ts index 5edb8366..f825d6b4 100644 --- a/chartlets.js/packages/lib/src/types/state/store.ts +++ b/chartlets.js/packages/lib/src/types/state/store.ts @@ -25,4 +25,10 @@ export interface StoreState { * See hook `useThemeMode()`. */ themeMode?: ThemeMode; + /** + * Store last input values for callback requests to avoid invoking them if + * there are no state changes + */ + lastCallbackInputValues: Record; + loadingState: Record; } diff --git a/chartlets.js/packages/lib/src/utils/compare.test.ts b/chartlets.js/packages/lib/src/utils/compare.test.ts new file mode 100644 index 00000000..5889c4b9 --- /dev/null +++ b/chartlets.js/packages/lib/src/utils/compare.test.ts @@ -0,0 +1,38 @@ +import { describe, expect, it } from "vitest"; +import { shallowEqualArrays } from "@/utils/compare"; + +describe("Test shallowEqualArrays()", () => { + const arr_a: string[] = ["a", "b", "c"]; + const arr_b: string[] = ["a", "b", "c"]; + const arr_c: string[] = ["a", "b", "d"]; + const arr_d: string[] = []; + const arr_e: string[] = ["a", "b", "c", "d"]; + const arr_f: (string | null)[] = ["a", "b", "c", null]; + const arr_g: (string | null)[] = ["a", "b", "c", null]; + const arr_h = [1, [1, 2, 3], [4, 5, 6]]; + const arr_i = [1, [1, 2, 3], [4, 5, 6]]; + const arr_j: (number | string | null)[] = [1, 2, "c", null]; + const arr_k: (number | string | null)[] = [1, 3, "c", null]; + const arr_l: (number | string | null)[] = [1, 2, "c", null]; + const arr_m: number[] = [1, 2]; + const arr_n: number[] = [1, 2]; + const arr_o: null[] = [null]; + const arr_p: null[] = [null]; + const arr_q: null[] = []; + it("works", () => { + expect(shallowEqualArrays(arr_a, arr_b)).toBe(true); + expect(shallowEqualArrays(arr_a, arr_c)).toBe(false); + expect(shallowEqualArrays(arr_a, arr_d)).toBe(false); + expect(shallowEqualArrays(arr_a, arr_e)).toBe(false); + expect(shallowEqualArrays(arr_e, arr_c)).toBe(false); + expect(shallowEqualArrays(arr_f, arr_g)).toBe(true); + expect(shallowEqualArrays(arr_h, arr_i)).toBe(false); + expect(shallowEqualArrays(arr_j, arr_k)).toBe(false); + expect(shallowEqualArrays(arr_j, arr_l)).toBe(true); + expect(shallowEqualArrays(arr_m, arr_n)).toBe(true); + expect(shallowEqualArrays(arr_m, arr_l)).toBe(false); + expect(shallowEqualArrays(arr_o, arr_p)).toBe(true); + expect(shallowEqualArrays(arr_p, arr_q)).toBe(false); + expect(shallowEqualArrays(arr_p)).toBe(false); + }); +}); diff --git a/chartlets.js/packages/lib/src/utils/compare.ts b/chartlets.js/packages/lib/src/utils/compare.ts new file mode 100644 index 00000000..eb917bc9 --- /dev/null +++ b/chartlets.js/packages/lib/src/utils/compare.ts @@ -0,0 +1,12 @@ +export function shallowEqualArrays( + arr1?: unknown[], + arr2?: unknown[], +): boolean { + if (!arr1 || !arr2) { + return false; + } + if (arr1.length !== arr2.length) { + return false; + } + return arr1.every((val, index) => val === arr2[index]); +} diff --git a/chartlets.py/chartlets/components/skeleton.py b/chartlets.py/chartlets/components/skeleton.py index ebd59427..a41b0c30 100644 --- a/chartlets.py/chartlets/components/skeleton.py +++ b/chartlets.py/chartlets/components/skeleton.py @@ -25,3 +25,11 @@ class Skeleton(Component): - 'wave': A shimmering animation. - False: No animation. """ + + opacity: float | None = None + """Opacity to change what is seen during the load time. + If opacity is set to 1, it will hide everything behind it. + If opacity is less than 1 but not 0, it provides a opaque view of the + background + If opacity is set to 0, it still shows the minimal amount of skeleton. + """ From e95b31399ac5c46029b191ef54e11dbf02b05c25 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:00:55 +0100 Subject: [PATCH 09/18] add comment --- chartlets.js/packages/lib/src/types/state/store.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chartlets.js/packages/lib/src/types/state/store.ts b/chartlets.js/packages/lib/src/types/state/store.ts index f825d6b4..0085ab10 100644 --- a/chartlets.js/packages/lib/src/types/state/store.ts +++ b/chartlets.js/packages/lib/src/types/state/store.ts @@ -30,5 +30,9 @@ export interface StoreState { * there are no state changes */ lastCallbackInputValues: Record; + /** + * Store the loading state of each output ID of the callback that is invoked. + * If the request fails, the state is set to `failed` + */ loadingState: Record; } From 667c40341afc0f72ff9bf75531971bdef868041c Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:38:42 +0100 Subject: [PATCH 10/18] fix tests --- .../actions/handleHostStoreChanges.test.tsx | 2 ++ .../lib/src/plugins/mui/Skeleton.test.tsx | 22 +++++-------------- 2 files changed, 7 insertions(+), 17 deletions(-) diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx index 712abaf6..c2708008 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx @@ -201,6 +201,7 @@ describe("handleHostStoreChange", () => { expect(result[0]).toEqual({ ...propertyRefs[0], inputValues: ["CHL"], + outputIds: [], }); // second call -> memoized -> should not create callback request @@ -213,6 +214,7 @@ describe("handleHostStoreChange", () => { expect(result[0]).toEqual({ ...propertyRefs[0], inputValues: ["TMP"], + outputIds: [], }); // fourth call -> memoized -> should not invoke callback diff --git a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx index bac05717..b6c2b08d 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.test.tsx @@ -4,21 +4,15 @@ import { Skeleton } from "@/plugins/mui/Skeleton"; describe("Skeleton", () => { it("should render the MUI Skeleton when loading is true", () => { - render(); + render(); const muiSkeleton = screen.getByTestId("skeleton-test-id"); expect(muiSkeleton).toBeInTheDocument(); expect(muiSkeleton).toHaveClass("MuiSkeleton-root"); }); - it("should not render the MUI Skeleton without skeletonProps", () => { - render(); - const muiSkeleton = screen.queryByTestId("skeleton-test-id"); - expect(muiSkeleton).not.toBeInTheDocument(); - }); - it("should not render the MUI Skeleton and render children when loading is false", () => { render( - +
Test Content
, ); @@ -28,26 +22,20 @@ describe("Skeleton", () => { }); it("should render with the specified variant", () => { - render(); + render(); const muiSkeleton = screen.getByTestId("skeleton-test-id"); expect(muiSkeleton).toHaveClass("MuiSkeleton-circular"); }); it("should render with specified width and height", () => { - render(); + render(); const muiSkeleton = screen.getByTestId("skeleton-test-id"); expect(muiSkeleton).toHaveStyle("width: 100px"); expect(muiSkeleton).toHaveStyle("height: 50px"); }); - it("should render with specified style", () => { - render(); - const muiSkeleton = screen.getByTestId("skeleton-test-id"); - expect(muiSkeleton).toHaveStyle("background-color: rgb(255, 0, 0);"); - }); - it("should render with specified animation", () => { - render(); + render(); const muiSkeleton = screen.getByTestId("skeleton-test-id"); expect(muiSkeleton).toHaveClass("MuiSkeleton-wave"); }); From 9689c2624a53b341a004118821b302db468cd16a Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:39:10 +0100 Subject: [PATCH 11/18] update component.py --- chartlets.py/chartlets/component.py | 4 ++++ chartlets.py/chartlets/components/charts/vega.py | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/chartlets.py/chartlets/component.py b/chartlets.py/chartlets/component.py index 08dbf1d1..38e8da06 100644 --- a/chartlets.py/chartlets/component.py +++ b/chartlets.py/chartlets/component.py @@ -37,6 +37,10 @@ class Component(ABC): children: list[Union["Component", str, None]] | None = None """Children used by many specific components. Optional.""" + skeletonProps: dict[str, Any] | None = None + """Add the skeleton props from the Skeleton MUI component to render a + skeleton during long loading times.""" + @property def type(self): return self.__class__.__name__ diff --git a/chartlets.py/chartlets/components/charts/vega.py b/chartlets.py/chartlets/components/charts/vega.py index b96e5ea6..45e6f929 100644 --- a/chartlets.py/chartlets/components/charts/vega.py +++ b/chartlets.py/chartlets/components/charts/vega.py @@ -36,10 +36,6 @@ class VegaChart(Component): chart: altair.Chart | None = None """The [Vega Altair chart](https://altair-viz.github.io/gallery/index.html).""" - skeletonProps: dict[str, Any] | None = None - """Add the skeleton props from the Skeleton MUI component to render a - skeleton during long loading times.""" - def to_dict(self) -> dict[str, Any]: d = super().to_dict() if self.chart is not None: From 4a5af08920438bb8029ec13bfe2e6f5b8bdb33c3 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:39:37 +0100 Subject: [PATCH 12/18] remove console logs --- chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx index 9269cc99..b80903c1 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Skeleton.tsx @@ -25,12 +25,11 @@ export const Skeleton = ({ isLoading, ...props }: SkeletonProps) => { + // Set default values if not available const opacity: number = props.opacity ?? 0.7; props.width = props.width ?? "100%"; props.height = props.height ?? "100%"; - console.log("opacity", opacity); - console.log("props.width", props.width); - console.log("props.height", props.height); + return (
{children} From 4d34fb42ed9bfc89d76b81b6178b3b8d14787356 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:40:33 +0100 Subject: [PATCH 13/18] add skeleton to table --- .../packages/lib/src/plugins/mui/Table.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/chartlets.js/packages/lib/src/plugins/mui/Table.tsx b/chartlets.js/packages/lib/src/plugins/mui/Table.tsx index c5dbd78e..a3ae752a 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Table.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Table.tsx @@ -9,6 +9,9 @@ import { } from "@mui/material"; import type { ComponentProps, ComponentState } from "@/index"; import type { SxProps } from "@mui/system"; +import { Skeleton } from "@/plugins/mui/Skeleton"; +import type { ReactElement } from "react"; +import { useLoadingState } from "@/hooks"; interface TableCellProps { id: string | number; @@ -38,8 +41,19 @@ export const Table = ({ columns, hover, stickyHeader, + skeletonProps, onChange, }: TableProps) => { + const loadingState = useLoadingState(); + console.log("loadingState", loadingState); + if (!id) { + return; + } + const isLoading = loadingState[id]; + if (isLoading == "failed") { + return
An error occurred while loading the data.
; + } + if (!columns || columns.length === 0) { return
No columns provided.
; } @@ -69,7 +83,7 @@ export const Table = ({ } }; - return ( + const table: ReactElement | null = ( @@ -107,4 +121,20 @@ export const Table = ({ ); + + const isSkeletonRequired = skeletonProps !== undefined; + if (!isSkeletonRequired) { + return table; + } + const skeletonId = id + "-skeleton"; + return ( + + {table} + + ); }; From 016f5157c53bcd38d76626c127d1de275eb7ae95 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:42:02 +0100 Subject: [PATCH 14/18] update demo to add lag to show skeleton --- chartlets.py/demo/my_extension/my_panel_1.py | 9 +++- chartlets.py/demo/my_extension/my_panel_6.py | 47 ++++++++++++++++---- 2 files changed, 47 insertions(+), 9 deletions(-) diff --git a/chartlets.py/demo/my_extension/my_panel_1.py b/chartlets.py/demo/my_extension/my_panel_1.py index 6242c683..0d46ee4f 100644 --- a/chartlets.py/demo/my_extension/my_panel_1.py +++ b/chartlets.py/demo/my_extension/my_panel_1.py @@ -1,3 +1,4 @@ +import time from typing import Any import altair as alt from chartlets import Component, Input, Output, State @@ -13,7 +14,9 @@ @panel.layout() def render_panel(ctx: Context) -> Component: selected_dataset: int = 0 - chart_skeleton = Skeleton(height="100px", variant="rounded", animation="wave") + chart_skeleton = Skeleton( + height="100%", width="100%", variant="rounded", animation="wave", opacity=0.1 + ) chart = VegaChart( id="chart", chart=make_chart(ctx, selected_dataset), @@ -56,6 +59,8 @@ def render_panel(ctx: Context) -> Component: def make_chart(ctx: Context, selected_dataset: int = 0) -> alt.Chart: dataset_key = tuple(ctx.datasets.keys())[selected_dataset] dataset = ctx.datasets[dataset_key] + # simulate lag to show skeleton + time.sleep(5) variable_name = "a" if selected_dataset == 0 else "u" @@ -107,6 +112,8 @@ def get_click_event_points( that was clicked. """ + # simulate lag to show skeleton + time.sleep(5) if points: conditions = [] for field, values in points.items(): diff --git a/chartlets.py/demo/my_extension/my_panel_6.py b/chartlets.py/demo/my_extension/my_panel_6.py index 7622c678..e6e9672a 100644 --- a/chartlets.py/demo/my_extension/my_panel_6.py +++ b/chartlets.py/demo/my_extension/my_panel_6.py @@ -1,5 +1,7 @@ +import time + from chartlets import Component, Input, Output -from chartlets.components import Box, Typography, Table +from chartlets.components import Box, Typography, Table, Skeleton, Button from server.context import Context from server.panel import Panel @@ -32,11 +34,23 @@ def render_panel( ["3", "Peter", "Jones", 40], ] - table = Table(id="table", rows=rows, columns=columns, hover=True) + table_skeleton = Skeleton( + height="100%", width="100%", variant="rounded", animation="wave", opacity=0.1 + ) + + table = Table( + id="table", + rows=rows, + columns=columns, + hover=True, + skeletonProps=table_skeleton.to_dict(), + ) title_text = Typography(id="title_text", children=["Basic Table"]) info_text = Typography(id="info_text", children=["Click on any row."]) + update_button = Button(id="update_button", text="Update Table") + return Box( style={ "display": "flex", @@ -45,14 +59,31 @@ def render_panel( "height": "100%", "gap": "6px", }, - children=[title_text, table, info_text], + children=[title_text, table, update_button, info_text], ) # noinspection PyUnusedLocal -@panel.callback(Input("table"), Output("info_text", "children")) -def update_info_text( - ctx: Context, - table_row: int, -) -> list[str]: +@panel.callback( + Input("table"), + Output("info_text", "children"), +) +def update_info_text(ctx: Context, table_row: int) -> list[str]: + time.sleep(3) + return [f"The clicked row value is {table_row}."] + + +@panel.callback( + Input("update_button", "clicked"), + Output("table", "rows"), +) +def update_table_rows(ctx: Context, update_button_clicked) -> TableRow: + # simulate lag to show skeleton + time.sleep(3) + rows: TableRow = [ + ["1", "John", "Smith", 94], + ["2", "Jane", "Jones", 5], + ["3", "Peter", "Doe", 40.5], + ] + return rows From 9adf3a0d09dcf119a4b0ae7a7852c336756defc7 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:44:14 +0100 Subject: [PATCH 15/18] update CHANGES.md --- chartlets.js/CHANGES.md | 4 +++- chartlets.py/CHANGES.md | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/chartlets.js/CHANGES.md b/chartlets.js/CHANGES.md index 4527d3ae..2eeb0fac 100644 --- a/chartlets.js/CHANGES.md +++ b/chartlets.js/CHANGES.md @@ -6,6 +6,9 @@ * Callbacks will now only be invoked when there’s an actual change in state, reducing unnecessary processing and improving performance. (#112) +* New (MUI) components + - `Skeleton` (currently supported for Vega Charts and Tables) + ## Version 0.1.4 (from 2025/03/06) * In `chartlets.js` we no longer emit warnings and errors in common @@ -68,7 +71,6 @@ - `Switch` - `Tabs` - `Slider` - - `Skeleton` (currently supported for Vega Charts) * Supporting `tooltip` property for interactive MUI components. diff --git a/chartlets.py/CHANGES.md b/chartlets.py/CHANGES.md index 70fe2ac1..1f521854 100644 --- a/chartlets.py/CHANGES.md +++ b/chartlets.py/CHANGES.md @@ -2,6 +2,8 @@ * Add `multiple` property for `Select` component to enable the of multiple elements. +* New (MUI) components + - `Skeleton` (currently supported for Vega Charts and Tables) ## Version 0.1.4 (from 2025/03/06) From 2065bb11b21fc9027dad60a7b64a4b8ce2527387 Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 18 Mar 2025 15:45:17 +0100 Subject: [PATCH 16/18] remove console log --- chartlets.js/packages/lib/src/plugins/mui/Table.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/chartlets.js/packages/lib/src/plugins/mui/Table.tsx b/chartlets.js/packages/lib/src/plugins/mui/Table.tsx index a3ae752a..d52bddf4 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Table.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Table.tsx @@ -45,7 +45,6 @@ export const Table = ({ onChange, }: TableProps) => { const loadingState = useLoadingState(); - console.log("loadingState", loadingState); if (!id) { return; } From 80fd27d2c8100c0177ede9ac13c8c7395df56bdf Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 15 Apr 2025 11:01:00 +0200 Subject: [PATCH 17/18] fix test --- .../packages/lib/src/actions/handleHostStoreChanges.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx index 3c42543e..8325bc2e 100644 --- a/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx +++ b/chartlets.js/packages/lib/src/actions/handleHostStoreChanges.test.tsx @@ -190,6 +190,7 @@ describe("handleHostStoreChange", () => { inputIndex: 0, inputValues: ["CHL"], property: "variableName", + outputIds: ["select"], }, ]); From f6ccf456150dbb20e6f7946109d985f531ca5cfa Mon Sep 17 00:00:00 2001 From: Yogesh Kumar Baljeet Singh Date: Tue, 15 Apr 2025 11:51:34 +0200 Subject: [PATCH 18/18] add tests and improve coverage --- .../lib/src/plugins/mui/Table.test.tsx | 26 +++++++++++++++++++ .../lib/src/plugins/vega/VegaChart.test.tsx | 17 ++++++++++-- .../lib/src/plugins/vega/VegaChart.tsx | 25 +++++++++++------- 3 files changed, 56 insertions(+), 12 deletions(-) diff --git a/chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx b/chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx index e196d301..231ad37b 100644 --- a/chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx +++ b/chartlets.js/packages/lib/src/plugins/mui/Table.test.tsx @@ -73,4 +73,30 @@ describe("Table", () => { }, }); }); + + it("should not render the Table component when no id provided", () => { + render( {}} />); + + const table = screen.queryByRole("table"); + expect(table).toBeNull(); + }); + + it( + "should render the Table component with skeleton when skeletonProps are" + + " provided", + () => { + render( +
{}} + skeletonProps={{ variant: "rectangular" }} + />, + ); + + const table = screen.queryByRole("table"); + expect(table).toBeNull(); + }, + ); }); diff --git a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.test.tsx b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.test.tsx index 99e29ec3..44d76cfc 100644 --- a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.test.tsx +++ b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.test.tsx @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { render } from "@testing-library/react"; +import { render, screen, waitFor } from "@testing-library/react"; // import { render, screen, fireEvent } from "@testing-library/react"; import type { TopLevelSpec } from "vega-lite"; import { createChangeHandler } from "@/plugins/mui/common.test"; @@ -21,7 +21,7 @@ describe("VegaChart", () => { expect(document.querySelector("#vc")).not.toBeUndefined(); }); - it("should render if chart is given", () => { + it("should render if chart is given", async () => { const { recordedEvents, onChange } = createChangeHandler(); render( { // expect(document.body).toEqual({}); expect(recordedEvents.length).toBe(0); + const test_chart = screen.queryByTestId("vega-test-id"); + expect(test_chart).toBeDefined(); + const canvas = await waitFor(() => screen.getByRole("graphics-document")); + expect(canvas).toBeInTheDocument(); + // TODO: all of the following doesn't work! // expect(document.querySelector("canvas")).toEqual({}); // expect(screen.getByRole("canvas")).not.toBeUndefined(); // fireEvent.click(screen.getByRole("canvas")); // expect(recordedEvents.length).toBe(1); }); + + it("should not render if id is not given", () => { + const { recordedEvents, onChange } = createChangeHandler(); + render(); + expect(recordedEvents.length).toBe(0); + const test_chart = screen.queryByTestId("vega-test-id"); + expect(test_chart).toBeNull(); + }); }); const chart: TopLevelSpec = { diff --git a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx index be4accd5..cd815850 100644 --- a/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx +++ b/chartlets.js/packages/lib/src/plugins/vega/VegaChart.tsx @@ -42,21 +42,26 @@ export function VegaChart({ } const chart: ReactElement | null = initialChart ? ( -
- + -
+ ) : ( -
+
); const isSkeletonRequired = skeletonProps !== undefined; if (!isSkeletonRequired) { - return chart; + return chart; } const skeletonId = id + "-skeleton"; return (