Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WC-2678]: Add playground to Custom Chart #1436

Open
wants to merge 22 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
984b9a7
chore: copy mobx kit
samuelreichert Feb 7, 2025
a75fc10
chore: convert hook to controller
iobuhov Feb 5, 2025
14d5ecf
chore: add mergeRefs
iobuhov Feb 5, 2025
3609643
chore: change method to arrow fn
iobuhov Feb 5, 2025
63a3b0f
feat(custom-chart): add onClick with data attribute
samuelreichert Feb 11, 2025
3bbd45e
fix(custom-chart): use correct file path
samuelreichert Feb 11, 2025
f11acc0
feat(custom-chart): add shared-charts lib
samuelreichert Feb 17, 2025
3f4ab10
feat(custom-chart): add playground mode to custom-chart
samuelreichert Feb 17, 2025
af1037d
fix(charts): fix typo
samuelreichert Feb 17, 2025
e0e28fe
feat(charts): add more exports on shared charts
samuelreichert Feb 17, 2025
044f068
fix(charts): fix typo
samuelreichert Feb 18, 2025
3920bdd
feat(custom-chart): add playground context to Custom Chart
samuelreichert Feb 18, 2025
a47df20
feat(custom-chart): add playground on preview mode
samuelreichert Feb 18, 2025
4371749
fix(custom-chart): fix preview mode on custom chart
samuelreichert Feb 18, 2025
1f25272
feat(custom-chart): move logic to useCustomChart and merge playground…
samuelreichert Feb 18, 2025
e255f3c
feat(mobx): update GateProvider
samuelreichert Feb 18, 2025
f2c648e
feat(custom-chart): rename Host to CustomChartControllerHost
samuelreichert Feb 18, 2025
3d428ba
feat(custom-chart): create mergePlaygroundState fucntion
samuelreichert Feb 18, 2025
d9fe79e
feat(custom-chart): create mergeRefs function
samuelreichert Feb 18, 2025
cb834a7
feat(custom-chart): create chart controller
samuelreichert Feb 18, 2025
22a907e
feat(custom-chart): refactor custom chart to use new controller
samuelreichert Feb 18, 2025
f36d398
fix(custom-chart): remove ref from chart controller
samuelreichert Feb 19, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/pluggableWidgets/custom-chart-web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@
"cross-env": "^7.0.3"
},
"dependencies": {
"@mendix/shared-charts": "workspace:*",
"@mendix/widget-plugin-mobx-kit": "workspace:*",
"classnames": "^2.3.2",
"plotly.js-dist-min": "^2.35.3"
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,19 @@
import { Properties } from "@mendix/pluggable-widgets-tools";
import { hidePropertyIn, Problem, Properties } from "@mendix/pluggable-widgets-tools";
import { checkSlot, withPlaygroundSlot } from "@mendix/shared-charts/preview";
import {
StructurePreviewProps,
structurePreviewPalette
structurePreviewPalette,
StructurePreviewProps
} from "@mendix/widget-plugin-platform/preview/structure-preview-api";
import { CustomChartPreviewProps } from "../typings/CustomChartProps";

export function getProperties(_values: CustomChartPreviewProps, defaultProperties: Properties): Properties {
export function getProperties(values: CustomChartPreviewProps, defaultProperties: Properties): Properties {
if (values.showPlaygroundSlot === false) {
hidePropertyIn(defaultProperties, values, "playground");
}
return defaultProperties;
}

export function getPreview(_values: CustomChartPreviewProps, isDarkMode: boolean): StructurePreviewProps {
export function getPreview(values: CustomChartPreviewProps, isDarkMode: boolean): StructurePreviewProps {
const palette = structurePreviewPalette[isDarkMode ? "dark" : "light"];
const sampleChartSvg = `
<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
Expand All @@ -25,7 +29,7 @@ export function getPreview(_values: CustomChartPreviewProps, isDarkMode: boolean
</svg>
`;

return {
const preview: StructurePreviewProps = {
type: "Container",
backgroundColor: palette.background.container,
borders: true,
Expand Down Expand Up @@ -59,4 +63,14 @@ export function getPreview(_values: CustomChartPreviewProps, isDarkMode: boolean
}
]
};

return withPlaygroundSlot(values, preview);
}

export function check(values: CustomChartPreviewProps): Problem[] {
const errors: Array<Problem | Problem[]> = [];

errors.push(checkSlot(values));

return errors.flat();
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const defaultSampleData = `[{
"x": [1, 2, 3, 4, 5, 6, 7, 8],
"y": [5, 35, 15, 45, 25, 65, 30, 55],
"mode": "lines+markers",
"marker": {
"marker": {
"color": "#264AE5",
"size": 8
},
Expand Down Expand Up @@ -39,14 +39,16 @@ const defaultConfig = `{
}`;

export function preview(props: CustomChartPreviewProps): ReactElement {
const { renderer: PlaygroundSlot } = props.playground ?? { renderer: () => null };

const containerProps = {
name: "preview-custom-chart",
class: props.class,
style: props.styleObject,
tabIndex: 0,
dataStatic: props.dataStatic || defaultSampleData,
sampleData: props.sampleData,
devMode: props.devMode,
showPlaygroundSlot: props.showPlaygroundSlot,
layoutStatic: props.layoutStatic || defaultSampleLayout,
sampleLayout: props.sampleLayout,
configurationOptions: props.configurationOptions || defaultConfig,
Expand All @@ -56,5 +58,25 @@ export function preview(props: CustomChartPreviewProps): ReactElement {
height: props.height || 75
};

return <CustomChart {...containerProps} />;
return (
<div style={{ display: "flex", flexFlow: "column nowrap" }}>
<div
style={
props.showPlaygroundSlot
? undefined
: {
display: "none"
}
}
>
<PlaygroundSlot caption="Playground slot">{dropzone()}</PlaygroundSlot>
</div>
<CustomChart {...containerProps} />
</div>
);
}

// Preview don't support React component as children. So we forced to use plain function.
const dropzone = (): React.ReactNode => (
<div style={{ padding: "10px 10px 10px 0", display: "flex", justifyContent: "end", flexGrow: 1, height: 58 }} />
);
20 changes: 9 additions & 11 deletions packages/pluggableWidgets/custom-chart-web/src/CustomChart.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
import { ReactElement, createElement } from "react";
import { getPlaygroundContext } from "@mendix/shared-charts/main";
import { createElement, Fragment, ReactElement } from "react";
import { CustomChartContainerProps } from "../typings/CustomChartProps";
import { useCustomChart } from "./hooks/useCustomChart";
import { useActionEvents } from "./hooks/useActionEvents";
import "./ui/CustomChart.scss";

const PlaygroundContext = getPlaygroundContext();

export default function CustomChart(props: CustomChartContainerProps): ReactElement {
const { chartRef, containerStyle } = useCustomChart(props);
const { handleClick } = useActionEvents(props);
const { containerStyle, playgroundData, ref } = useCustomChart(props);

return (
<div
ref={chartRef}
className="widget-custom-chart"
style={containerStyle}
tabIndex={props.tabIndex}
onClick={handleClick}
/>
<Fragment>
<div ref={ref} className="widget-custom-chart" style={containerStyle} tabIndex={props.tabIndex} />
<PlaygroundContext.Provider value={playgroundData}>{props.playground}</PlaygroundContext.Provider>
</Fragment>
);
}
17 changes: 16 additions & 1 deletion packages/pluggableWidgets/custom-chart-web/src/CustomChart.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,21 @@
<caption>Sample data</caption>
<description>Data for preview. It will be merged with the 'Static data' in the web modeler or at runtime when no 'Source attribute' is selected</description>
</property>
<property key="devMode" type="enumeration" defaultValue="developer">
<!-- <property key="devMode" type="enumeration" defaultValue="developer">
<caption>Mode</caption>
<description>The development mode adds a button to the charts when running the app which can be used to toggle a live editor for the advanced configuration options</description>
<enumerationValues>
<enumerationValue key="developer">Development</enumerationValue>
<enumerationValue key="advanced">Production</enumerationValue>
</enumerationValues>
</property> -->
<property key="showPlaygroundSlot" type="boolean" defaultValue="false">
<caption>Show playground slot</caption>
<description />
</property>
<property key="playground" type="widgets" required="false">
<caption>Playground slot</caption>
<description />
</property>
</propertyGroup>
<propertyGroup caption="Layout options">
Expand Down Expand Up @@ -85,6 +93,13 @@
<caption>On click</caption>
<description />
</property>
<property key="eventDataAttribute" type="attribute" required="false">
<caption>Event data attribute</caption>
<description>The attribute to store received raw data from the chart event. https://plot.ly/javascript/plotlyjs-events/#event-data</description>
<attributeTypes>
<attributeType name="String" />
</attributeTypes>
</property>
<!-- <property key="eventEntity" type="association" required="false" setLabel="true">
<caption>Event entity</caption>
<description>The entity used to pass the event data to the server</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export interface ChartProps {
config: Partial<Config>;
width: number;
height: number;
onClick?: (data: any) => void;
}

export class PlotlyChart {
Expand All @@ -20,13 +21,16 @@ export class PlotlyChart {
this.data = props.data;
this.layout = props.layout;
this.config = props.config;
this.init();
this.init(props);
}

private init(): void {
private init(props: ChartProps): void {
newPlot(this.element, this.data, this.layout, this.config)
.then(plotlyElement => {
this.plotlyElement = plotlyElement;
if (props?.onClick) {
this.plotlyElement.on("plotly_click", props.onClick);
}
})
.catch(error => {
console.error("Error initializing chart:", error);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { ReactiveController } from "@mendix/widget-plugin-mobx-kit/main";
import { debounce } from "@mendix/widget-plugin-platform/utils/debounce";
import { makeObservable, observable } from "mobx";
import { Config, Data, Layout } from "plotly.js-dist-min";
import { CustomChartContainerProps } from "../../typings/CustomChartProps";
import { ChartProps, PlotlyChart } from "../components/PlotlyChart";
import { parseConfig, parseData, parseLayout } from "../utils/utils";
import { CustomChartControllerHost } from "./CustomChartControllerHost";
import { ResizeController } from "./ResizeController";

export class ChartController implements ReactiveController {
private cleanup: undefined | (() => void) = undefined;
private configurationOptions: string;
private dataAttribute?: string;
private dataStatic: string;
private layoutAttribute?: string;
private layoutStatic: string;
private sampleData: string;
private sampleLayout: string;
private chart: PlotlyChart | null;

constructor(host: CustomChartControllerHost, props: CustomChartContainerProps) {
host.addController(this);

this.configurationOptions = props.configurationOptions;
this.dataAttribute = props.dataAttribute?.value;
this.dataStatic = props.dataStatic;
this.layoutAttribute = props.layoutAttribute?.value;
this.layoutStatic = props.layoutStatic;
this.sampleData = props.sampleData;
this.sampleLayout = props.sampleLayout;
this.chart = null;

makeObservable<
ChartController,
| "configurationOptions"
| "dataAttribute"
| "dataStatic"
| "layoutAttribute"
| "layoutStatic"
| "sampleData"
| "sampleLayout"
>(this, {
configurationOptions: observable,
dataAttribute: observable,
dataStatic: observable,
layoutAttribute: observable,
layoutStatic: observable,
sampleData: observable,
sampleLayout: observable
});
}

setup(): () => void {
return () => this.cleanup?.();
}

getChartData(onClick: (data: any) => void, resizeController: ResizeController): ChartProps {
const layout = this.getLayout();
return {
config: {
...this.getConfig(),
responsive: true
},
data: this.getData(),
layout: {
...layout,
width: resizeController.width,
height: resizeController.height,
autosize: true,
font: {
family: "Open Sans, sans-serif",
size: Math.max(12 * (resizeController.width / 1000), 8)
},
legend: {
...layout.legend,
font: {
...layout.legend?.font,
size: Math.max(10 * (resizeController.width / 1000), 7)
},
itemwidth: Math.max(10 * (resizeController.width / 1000), 3),
itemsizing: "constant"
},
xaxis: {
...layout.xaxis,
tickfont: {
...layout.xaxis?.tickfont,
size: Math.max(10 * (resizeController.width / 1000), 7)
}
},
yaxis: {
...layout.yaxis,
tickfont: {
...layout.yaxis?.tickfont,
size: Math.max(10 * (resizeController.width / 1000), 7)
}
},
margin: {
...layout.margin,
l: Math.max(50 * (resizeController.width / 1000), 30),
r: Math.max(50 * (resizeController.width / 1000), 30),
t: Math.max(50 * (resizeController.width / 1000), 30),
b: Math.max(50 * (resizeController.width / 1000), 30),
pad: Math.max(4 * (resizeController.width / 1000), 2)
}
},
onClick,

width: resizeController.width,
height: resizeController.height
};
}

getConfig(): Partial<Config> {
return parseConfig(this.configurationOptions);
}

getData(): Data[] {
return parseData(this.dataStatic, this.dataAttribute, this.sampleData);
}

getLayout(): Partial<Layout> {
return parseLayout(this.layoutStatic, this.layoutAttribute, this.sampleLayout);
}

setChart(target: HTMLDivElement, props: ChartProps): void {
const [setChartDebounced, abort] = debounce(() => {
if (!this.chart) {
this.chart = new PlotlyChart(target, props);
} else {
this.chart.update(props);
}
}, 100);

setChartDebounced();

this.cleanup = () => {
abort();
};
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { BaseControllerHost } from "@mendix/widget-plugin-mobx-kit/BaseControllerHost";
import { ResizeController } from "./ResizeController";
import { ChartController } from "./ChartController";
import { CustomChartContainerProps } from "../../typings/CustomChartProps";

export class CustomChartControllerHost extends BaseControllerHost {
resizeCtrl: ResizeController;
chartCtrl: ChartController;

constructor(props: CustomChartContainerProps) {
super();
this.resizeCtrl = new ResizeController(this);
this.chartCtrl = new ChartController(this, props);
}
}
Loading
Loading