feat(frontend): chartjs wizard (#2532)

* feat(frontend): chartjs wizard

* feat(frontend): fix indentation

* feat(frontend): done

* feat(frontend): remove console.log

* feat(frontend): fix dark mode + remove legacy components

* feat(frontend): reorder component list

* feat(frontend): fix dark mode
This commit is contained in:
Faton Ramadani
2023-10-31 21:12:44 +01:00
committed by GitHub
parent 7b7cf8b050
commit fc318724cf
12 changed files with 486 additions and 45 deletions

View File

@@ -0,0 +1,159 @@
<script lang="ts">
import { Chart } from 'svelte-chartjs'
import { Chart as ChartJS, registerables, type ChartOptions } from 'chart.js'
import RunnableWrapper from '../helpers/RunnableWrapper.svelte'
import type { AppInput } from '../../inputType'
import type {
AppViewerContext,
ComponentCustomCSS,
RichConfiguration,
RichConfigurations
} from '../../types'
import { initCss } from '../../utils'
import { getContext } from 'svelte'
import { initConfig, initOutput } from '../../editor/appUtils'
import { components } from '../../editor/component'
import ResolveConfig from '../helpers/ResolveConfig.svelte'
import { twMerge } from 'tailwind-merge'
import ResolveStyle from '../helpers/ResolveStyle.svelte'
export let id: string
export let componentInput: AppInput | undefined
export let configuration: RichConfigurations
export let initializing: boolean | undefined = undefined
export let customCss: ComponentCustomCSS<'piechartcomponent'> | undefined = undefined
export let render: boolean
export let datasets: RichConfiguration | undefined
export let xData: RichConfiguration | undefined
type Dataset = {
value: RichConfiguration
name: string
}
let resolvedDatasets: Dataset[]
let resolvedDatasetsValues: Array<number[]> = []
let resolvedXData: any[] = []
const { app, worldStore, darkMode } = getContext<AppViewerContext>('AppViewerContext')
const outputs = initOutput($worldStore, id, {
result: undefined,
loading: false
})
ChartJS.register(...registerables)
let result: undefined = undefined
const resolvedConfig = initConfig(
components['chartjscomponent'].initialData.configuration,
configuration
)
function hasScales() {
const type = resolvedConfig?.type as string
return type === 'bar' || type === 'line' || type === 'scatter' || type === 'bubble'
}
$: options = {
responsive: true,
animation: false,
maintainAspectRatio: false,
...(resolvedConfig.options ?? {}),
plugins: {
legend: {
labels: {
color: $darkMode ? '#fff' : '#000'
}
}
},
...(hasScales()
? {
scales: {
y: {
ticks: { color: $darkMode ? '#eee' : '#333' },
grid: { color: $darkMode ? '#555' : '#ddd' }
},
x: {
ticks: { color: $darkMode ? '#eee' : '#333' },
grid: { color: $darkMode ? '#555' : '#ddd' }
}
}
}
: {})
} as ChartOptions
$: data =
datasets && xData && resolvedDatasets
? {
labels: resolvedXData,
datasets: resolvedDatasets.map((d, index) => {
return {
label: d.name,
data: resolvedDatasetsValues[index]
}
})
}
: result
let css = initCss($app.css?.chartjscomponent, customCss)
</script>
{#if datasets}
<ResolveConfig
{id}
key={'datasets'}
bind:resolvedConfig={resolvedDatasets}
configuration={datasets}
/>
{/if}
{#if xData}
<ResolveConfig {id} key={'xData'} bind:resolvedConfig={resolvedXData} configuration={xData} />
{/if}
{#if resolvedDatasets}
{#each resolvedDatasets as resolvedDataset, index (resolvedDataset.name + index)}
<ResolveConfig
{id}
key={'datasets' + index}
bind:resolvedConfig={resolvedDatasetsValues[index]}
configuration={resolvedDataset.value}
/>
{/each}
{/if}
{#each Object.keys(components['chartjscomponentv2'].initialData.configuration) as key (key)}
<ResolveConfig
{id}
{key}
bind:resolvedConfig={resolvedConfig[key]}
configuration={configuration[key]}
/>
{/each}
{#each Object.keys(css ?? {}) as key (key)}
<ResolveStyle
{id}
{customCss}
{key}
bind:css={css[key]}
componentStyle={$app.css?.chartjscomponent}
/>
{/each}
<RunnableWrapper {outputs} {render} autoRefresh {componentInput} {id} bind:initializing bind:result>
<div
class={twMerge('w-full h-full', css?.container?.class, 'wm-chartjs')}
style={css?.container?.style ?? ''}
>
{#if data && resolvedConfig.type}
{#key resolvedConfig.type}
{#key options}
<Chart type={resolvedConfig.type} {data} {options} />
{/key}
{/key}
{/if}
</div>
</RunnableWrapper>

View File

@@ -55,6 +55,7 @@
import AppLogsComponent from '../../components/display/AppLogsComponent.svelte'
import AppFlowStatusComponent from '../../components/display/AppFlowStatusComponent.svelte'
import AppChartJs from '../../components/display/AppChartJs.svelte'
import AppChartJsV2 from '../../components/display/AppChartJsV2.svelte'
import AppQuillEditor from '../../components/inputs/AppQuillEditor.svelte'
import AppList from '../../components/layout/AppList.svelte'
import AppJobIdLogComponent from '../../components/display/AppJobIdLogComponent.svelte'
@@ -645,6 +646,17 @@
componentInput={component.componentInput}
{render}
/>
{:else if component.type === 'chartjscomponentv2'}
<AppChartJsV2
configuration={component.configuration}
id={component.id}
customCss={component.customCss}
bind:initializing
componentInput={component.componentInput}
datasets={component.datasets}
xData={component.xData}
{render}
/>
{:else if component.type === 'carousellistcomponent'}
<AppCarouselList
id={component.id}

View File

@@ -92,6 +92,10 @@ export type RunFormComponent = BaseComponent<'runformcomponent'>
export type BarChartComponent = BaseComponent<'barchartcomponent'>
export type PieChartComponent = BaseComponent<'piechartcomponent'>
export type ChartJsComponent = BaseComponent<'chartjscomponent'>
export type ChartJsComponentV2 = BaseComponent<'chartjscomponentv2'> & {
xData: RichConfiguration | undefined
datasets: RichConfiguration | undefined
}
export type ScatterChartComponent = BaseComponent<'scatterchartcomponent'>
@@ -208,6 +212,7 @@ export type TypedComponent =
| ChartJsComponent
| CarouselListComponent
| PlotlyComponentV2
| ChartJsComponentV2
export type AppComponent = BaseAppComponent & TypedComponent
@@ -1112,6 +1117,33 @@ export const components = {
}
}
},
chartjscomponentv2: {
name: 'ChartJs',
icon: PieChart,
documentationLink: `${documentationBaseUrl}/chartjs`,
dims: '2:8-6:8' as AppComponentDimensions,
customCss: {
container: { class: '', style: '' }
},
initialData: {
configuration: {
type: {
type: 'static',
fieldType: 'select',
selectOptions: selectOptions.chartTypeOptions,
value: 'pie'
},
options: {
type: 'static',
fieldType: 'object',
value: {},
tooltip: 'ChartJs options object'
}
},
componentInput: undefined
}
},
barchartcomponent: {
name: 'Bar/Line Chart',
icon: BarChart4,

View File

@@ -74,15 +74,7 @@ const tables: ComponentSet = {
const charts: ComponentSet = {
title: 'Charts',
components: [
'barchartcomponent',
'piechartcomponent',
'vegalitecomponent',
'plotlycomponentv2',
'scatterchartcomponent',
'timeseriescomponent',
'chartjscomponent'
]
components: ['plotlycomponentv2', 'chartjscomponentv2', 'vegalitecomponent']
} as const
export const COMPONENT_SETS = [layout, tabs, buttons, inputs, tables, display, charts] as const

View File

@@ -670,6 +670,9 @@ export const quickStyleProperties: Record<
chartjscomponent: {
container: containerDefaultProps
},
chartjscomponentv2: {
container: containerDefaultProps
},
vegalitecomponent: {},
containercomponent: {
container: containerDefaultProps

View File

@@ -66,6 +66,16 @@
.toString(16)
.padEnd(6, '0')}`
})
} else if (subFieldType === 'chartjs') {
value.push({
value: {
type: 'static',
fieldType: 'array',
subFieldType: 'number',
value: [2, 4, 5, 6]
},
name: 'New dataset'
})
}
} else {
value.push('')
@@ -181,6 +191,7 @@
subFieldType={raw ? 'object' : subFieldType}
bind:componentInput
bind:value={item.value}
on:remove={() => deleteElementByType(index)}
/>
</div>

View File

@@ -0,0 +1,58 @@
<script lang="ts">
import type { RichConfiguration } from '../../types'
import InputsSpecEditor from './InputsSpecEditor.svelte'
import PanelSection from './common/PanelSection.svelte'
export let datasets: RichConfiguration | undefined = undefined
export let xData: RichConfiguration | undefined = undefined
export let id: string
</script>
<PanelSection
title={`Plotly configuration`}
tooltip="The configuration is divided into two parts: X-axis data and an array of datasets. Each dataset hold the data fot he Y-axis and the configuration for the plot (type, color, etc)."
>
<div class="w-full flex flex-col gap-4">
{#if xData}
<InputsSpecEditor
key={`X-axis data`}
bind:componentInput={xData}
{id}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType={xData?.['fieldType']}
subFieldType={xData?.['subFieldType']}
format={xData?.['format']}
selectOptions={xData?.['selectOptions']}
tooltip={xData?.['tooltip']}
fileUpload={xData?.['fileUpload']}
placeholder={xData?.['placeholder']}
customTitle={xData?.['customTitle']}
displayType={false}
/>
{/if}
{#if datasets}
<InputsSpecEditor
key={`Dataset`}
bind:componentInput={datasets}
{id}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType={datasets?.['fieldType']}
subFieldType={datasets?.['subFieldType']}
format={datasets?.['format']}
selectOptions={datasets?.['selectOptions']}
tooltip="For each dataset, you can specify the data for the Y-axis and the configuration for the plot (type, color, etc). If you want to have an eval for every data point, you can switch to JSON mode."
fileUpload={datasets?.['fileUpload']}
placeholder={datasets?.['placeholder']}
customTitle={datasets?.['customTitle']}
displayType={false}
shouldFormatExpression={true}
allowTypeChange={false}
/>
{/if}
</div>
</PanelSection>

View File

@@ -4,6 +4,7 @@
import ToggleButtonGroup from '$lib/components/common/toggleButton-v2/ToggleButtonGroup.svelte'
import ToggleButton from '$lib/components/common/toggleButton-v2/ToggleButton.svelte'
import PlotlyRichEditor from './PlotlyRichEditor.svelte'
import ChartJSRichEditor from './ChartJSRichEditor.svelte'
import { onMount } from 'svelte'
import type { RichConfiguration } from '../../types'
import type { InputConnectionEval } from '../../inputType'
@@ -17,7 +18,7 @@
onMount(() => {
if (
component.type === 'plotlycomponentv2' &&
(component.type === 'plotlycomponentv2' || component.type === 'chartjscomponentv2') &&
component.componentInput === undefined &&
component.datasets === undefined
) {
@@ -37,30 +38,80 @@
color: string
}
function resolveConfiguration(
x: RichConfiguration | undefined,
connections: InputConnectionEval[]
): string | undefined {
if (!x) return undefined
if (x.type === 'evalv2') {
connections.push(...x.connections)
return x.expr
}
if (x.type === 'static') return `[${x.value}]`
return undefined
}
function handleSelected(selected: string) {
if (component.type !== 'plotlycomponentv2') return
if (component.type === 'plotlycomponentv2') {
if (selected === 'ui-editor') {
convertToUIEditorCallback = () => {
component.componentInput = undefined
setUpUIEditor()
}
if (selected === 'ui-editor') {
convertToUIEditorCallback = () => {
component.componentInput = undefined
setUpUIEditor()
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement
activeElement?.blur()
document.body.focus()
})
} else if (selected === 'json') {
convertToJson()
}
} else if (component.type === 'chartjscomponentv2') {
if (selected === 'ui-editor') {
convertToUIEditorCallback = () => {
component.componentInput = undefined
setUpUIEditor()
}
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement
activeElement?.blur()
document.body.focus()
})
} else if (selected === 'json') {
convertToJson(component)
setTimeout(() => {
const activeElement = document.activeElement as HTMLElement
activeElement?.blur()
document.body.focus()
})
} else if (selected === 'json') {
convertChartJSToJson()
}
}
}
function setUpUIEditor() {
if (component.type !== 'plotlycomponentv2') return
if (component.type === 'plotlycomponentv2') {
component.datasets = createPlotlyComponentDataset()
component.xData = createXData()
} else if (component.type === 'chartjscomponentv2') {
component.datasets = createChartjsComponentDataset()
component.xData = createXData()
}
}
component.datasets = createPlotlyComponentDataset()
component.xData = createXData()
function createChartjsComponentDataset(): RichConfiguration {
return {
type: 'static',
fieldType: 'array',
subFieldType: 'chartjs',
value: [
{
value: {
type: 'static',
fieldType: 'array',
subFieldType: 'number',
value: [25, 25, 50]
},
name: 'Dataset 1'
}
]
}
}
function createPlotlyComponentDataset(): RichConfiguration {
@@ -95,20 +146,11 @@
}
}
function resolveConfiguration(
x: RichConfiguration | undefined,
connections: InputConnectionEval[]
): string | undefined {
if (!x) return undefined
if (x.type === 'evalv2') {
connections.push(...x.connections)
return x.expr
function convertToJson() {
if (component.type !== 'plotlycomponentv2') {
return
}
if (x.type === 'static') return `[${x.value}]`
return undefined
}
function convertToJson(component: any) {
const connections: InputConnectionEval[] = []
const xDataResolved = resolveConfiguration(component.xData, connections)
@@ -133,6 +175,42 @@
component.xData = undefined
}
function convertChartJSToJson() {
if (component.type !== 'chartjscomponentv2') {
return
}
const connections: InputConnectionEval[] = []
const xDataResolved = resolveConfiguration(component.xData, connections)
if (component.datasets === undefined || component.datasets.type !== 'static') return
const datasets = component.datasets?.value
component.componentInput = {
type: 'evalv2',
fieldType: 'object',
noStatic: true,
expr: `({
\t"labels": ${xDataResolved},
\t"datasets": [\n${
datasets
.map(
(rawDataset: Dataset) => `\t\t{
\t\t\tdata:${resolveConfiguration(rawDataset.value, connections)},
\t\t\tlabel: "${rawDataset.name}"
\t\t}`
)
.join(',\n') || ''
}\n\t]
})`,
connections: connections.filter(Boolean)
}
component.datasets = undefined
component.xData = undefined
}
function datasetToJson(
dataset: Dataset,
xDataExpr: string | undefined,
@@ -151,7 +229,7 @@
}
</script>
{#if component.type === 'plotlycomponentv2'}
{#if component.type === 'plotlycomponentv2' || component.type === 'chartjscomponentv2'}
<div class="p-2">
<ToggleButtonGroup
bind:selected
@@ -174,11 +252,19 @@
{#if selected === 'ui-editor'}
{#key renderCount}
<PlotlyRichEditor
id={component.id}
bind:datasets={component.datasets}
bind:xData={component.xData}
/>
{#if component.type === 'plotlycomponentv2'}
<PlotlyRichEditor
id={component.id}
bind:datasets={component.datasets}
bind:xData={component.xData}
/>
{:else if component.type === 'chartjscomponentv2'}
<ChartJSRichEditor
id={component.id}
bind:datasets={component.datasets}
bind:xData={component.xData}
/>
{/if}
{/key}
{:else}
<slot />

View File

@@ -15,4 +15,4 @@
$: fakeComponentInput && (value = fakeComponentInput.value)
</script>
<StaticInputEditor fieldType={subFieldType} bind:componentInput={fakeComponentInput} />
<StaticInputEditor fieldType={subFieldType} bind:componentInput={fakeComponentInput} on:remove />

View File

@@ -16,6 +16,7 @@
import AgGridWizard from '$lib/components/wizards/AgGridWizard.svelte'
import TableColumnWizard from '$lib/components/wizards/TableColumnWizard.svelte'
import PlotlyWizard from '$lib/components/wizards/PlotlyWizard.svelte'
import ChartJSWizard from '$lib/components/wizards/ChartJSWizard.svelte'
export let componentInput: StaticInput<any> | undefined
export let fieldType: InputType | undefined = undefined
@@ -171,7 +172,7 @@
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<PlotlyWizard bind:value={componentInput.value}>
<PlotlyWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
@@ -183,6 +184,27 @@
</div>
</div>
</div>
{:else if fieldType === 'chartjs'}
<div class="flex flex-row rounded-md bg-surface items-center h-full">
<div class="relative w-full">
<input
class="text-xs px-2 border-y w-full flex flex-row items-center border-r rounded-r-md h-8"
bind:value={componentInput.value.name}
placeholder="Dataset name"
/>
<div class="absolute top-1 right-1">
<ChartJSWizard bind:value={componentInput.value} on:remove>
<svelte:fragment slot="trigger">
<Button color="light" size="xs2" nonCaptureEvent={true}>
<div class="flex flex-row items-center gap-2 text-xs font-normal">
<Settings size={16} />
</div>
</Button>
</svelte:fragment>
</ChartJSWizard>
</div>
</div>
</div>
{:else}
<div class="flex gap-1 relative w-full">
<textarea

View File

@@ -25,6 +25,7 @@ export type InputType =
| 'ag-grid'
| 'table-column'
| 'plotly'
| 'chartjs'
// Connection to an output of another component
// defined by the id of the component and the path of the output
@@ -192,6 +193,7 @@ export type AppInput =
| AppInputSpec<'array', object[], 'ag-grid'>
| AppInputSpec<'array', object[], 'table-column'>
| AppInputSpec<'array', object[], 'plotly'>
| AppInputSpec<'array', object[], 'chartjs'>
export type RowAppInput = Extract<AppInput, { type: 'row' }>
export type StaticAppInput = Extract<AppInput, { type: 'static' }>

View File

@@ -0,0 +1,64 @@
<script lang="ts">
import { createEventDispatcher, getContext } from 'svelte'
import { Popup } from '../common'
import Label from '../Label.svelte'
import { offset, flip, shift } from 'svelte-floating-ui/dom'
import Button from '../common/button/Button.svelte'
import InputsSpecEditor from '../apps/editor/settingsPanel/InputsSpecEditor.svelte'
import type { AppViewerContext, RichConfiguration } from '../apps/types'
const { selectedComponent } = getContext<AppViewerContext>('AppViewerContext')
type Dataset = {
value: RichConfiguration
name: string
}
export let value: Dataset | undefined = undefined
const dispatch = createEventDispatcher()
function removeDataset() {
dispatch('remove')
}
</script>
<Popup
floatingConfig={{
strategy: 'fixed',
placement: 'left-end',
middleware: [offset(8), flip(), shift()]
}}
containerClasses="border rounded-lg shadow-lg bg-surface p-4"
>
<svelte:fragment slot="button">
<slot name="trigger" />
</svelte:fragment>
{#if value}
<div class="flex flex-col w-96 p-2 gap-4">
<Label label="Name">
<input type="text" bind:value={value.name} />
</Label>
<InputsSpecEditor
key={'Data'}
bind:componentInput={value.value}
id={$selectedComponent?.[0] ?? ''}
userInputEnabled={false}
shouldCapitalize={true}
resourceOnly={false}
fieldType={value.value?.['fieldType']}
subFieldType={value.value?.['subFieldType']}
format={value.value?.['format']}
selectOptions={value.value?.['selectOptions']}
tooltip={value.value?.['tooltip']}
fileUpload={value.value?.['fileUpload']}
placeholder={value.value?.['placeholder']}
customTitle={value.value?.['customTitle']}
displayType={false}
/>
<Button color="red" size="xs" on:click={removeDataset}>Remove dataset</Button>
</div>
{/if}
</Popup>