diff --git a/apps/plotly_examples/README.md b/apps/plotly_examples/README.md index 2815ec8763..d50d03502e 100644 --- a/apps/plotly_examples/README.md +++ b/apps/plotly_examples/README.md @@ -72,3 +72,6 @@ First run the function **generate_visualization_scenarios()**. Then run **genera **253 - 277** - Large Dataset **278 - 302** - Locale based Dataset +## Map data to the chart types + +This function **get_chart_type_from_image()** generates mapping of plotly chart data to the chart types available in fluent charts. Before running this, generate screenshots by running Playwright tests by changing the **getByTestId** in DeclarativeChart.spec.ts to **plotly-plot** which will take screenshots of the plotly charts only. These are then mapped to the charts available in our fluent charting library using the above function. diff --git a/apps/plotly_examples/python-scripts/generate_plotly_schema.py b/apps/plotly_examples/python-scripts/generate_plotly_schema.py index 3f68bde794..150771a554 100644 --- a/apps/plotly_examples/python-scripts/generate_plotly_schema.py +++ b/apps/plotly_examples/python-scripts/generate_plotly_schema.py @@ -4,6 +4,7 @@ from azure.identity import ManagedIdentityCredential, DefaultAzureCredential, VisualStudioCodeCredential, InteractiveBrowserCredential, get_bearer_token_provider from openai import AzureOpenAI import random +import glob scenario_dir = 'detailed_scenarios' class Scenario(BaseModel): @@ -330,6 +331,54 @@ def call_llm_detailed_plotly_schema(scenario: str, id: int, suffix: str): return id +def get_chart_type_from_image(): + directory_path = os.path.join('..', 'tests', 'Plotly.spec.ts-snapshots') + files = glob.glob(os.path.join(directory_path, '*')) + + chart_types = {} + + for file_path in files: + # Extract the file number from the file path + file_name = os.path.basename(file_path) + file_number = file_name.split('-')[3] + # padding the file_number with zeros making it a 3 digit number + if file_number.isdigit(): + file_number = file_number.zfill(3) + + with open(file_path, 'rb') as image_file: + response_format={ "type": "json_object" } + messages = [ + { + "role": "system", + "content": "You are an image data analyst having expertize in predicting data visualizations." + } + , + { + "role": "user", + "content": f"""Refer the image {image_file} and predict the chart type that best fits the data among 'Area', 'Line', 'Donut', 'Pie', 'Guage', 'Horizontal Bar tWithAxis', 'Vertical Bar', 'Vertical Stacked Bar ', 'Grouped Vertical Bar', 'Heatmap', 'Sankey'. Mark the chart type as 'Others' if none of the above types match. Provide the output in the form of a json object. Categorize the images correctly based on the chart type. + For example: {{"chart_type": "Area"}}""" + } + ] + response = call_llm(messages, response_format) + data = json.loads(response) + retry_count = 0 + while 'chart_type' not in data and retry_count < 3: + response = call_llm(messages, response_format) + data = json.loads(response) + retry_count += 1 + if retry_count == 3: + print(f"Failed to get chart type for file {file_number}") + continue + chart_type = data['chart_type'] + + # Map the file number and chart type + chart_types[file_number] = chart_type + + chart_types_json = json.dumps(chart_types, indent=2) + + output_path = 'aggregated_chart_types.json' + with open(output_path, 'w') as output_file: + output_file.write(chart_types_json) file_path = 'scenarios_level1.json' @@ -344,4 +393,7 @@ def call_llm_detailed_plotly_schema(scenario: str, id: int, suffix: str): # generate_detailed_visualization_schemas() # Generate locale based schemas -generate_locale_visualization_schemas() \ No newline at end of file +# generate_locale_visualization_schemas() + +# Generate chart types from screenshots taken by Playwright +get_chart_type_from_image() \ No newline at end of file diff --git a/apps/plotly_examples/python-scripts/sort-by-id.py b/apps/plotly_examples/python-scripts/sort-by-id.py new file mode 100644 index 0000000000..7d40ae3aef --- /dev/null +++ b/apps/plotly_examples/python-scripts/sort-by-id.py @@ -0,0 +1,9 @@ +import json + +with open('aggregated_chart_types.json', 'r') as file: + data = json.load(file) + +sorted_data = {key: data[key] for key in sorted(data.keys(), key=lambda x: int(x))} + +with open('aggregated_chart_types_sorted.json', 'w') as file: + json.dump(sorted_data, file, indent=2) \ No newline at end of file diff --git a/apps/plotly_examples/src/App.tsx b/apps/plotly_examples/src/App.tsx index 63c6f0a20a..a95f060423 100644 --- a/apps/plotly_examples/src/App.tsx +++ b/apps/plotly_examples/src/App.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { - FluentProvider, +import { + FluentProvider, webLightTheme, webDarkTheme, Dropdown, @@ -8,34 +8,69 @@ import { SelectionEvents, OptionOnSelectData, Subtitle2, - Body2 + Body2, + Switch, + Slider } from '@fluentui/react-components'; import { PortalCompatProvider } from '@fluentui/react-portal-compat'; import { ChartWrapper } from './components/ChartWrapper'; import { getSelection, saveSelection } from './components/utils'; +import { setRTL } from '@fluentui/react/lib/Utilities'; +import { SliderOnChangeData } from '@fluentui/react-components'; const App: React.FC = () => { const [value, setValue] = React.useState(getSelection("Theme", "Light")); + const [isRTL, setisRTL] = React.useState(getSelection("RTL", "false") === "true"); + const [labelRTLMode, setLabelRTLMode] = React.useState("Enable RTL"); + const [chartWidth, setChartWidth] = React.useState(Number(getSelection("ChartWidth", window.innerWidth.toString()))); + setRTL(isRTL); const onOptionSelect = (event: SelectionEvents, data: OptionOnSelectData): void => { setValue(data.optionText ?? "Light"); saveSelection("Theme", data.optionText ?? "Light"); }; + const handleRTLSwitchChange = () => { + const newIsRTL = !isRTL; + setisRTL(newIsRTL); + setLabelRTLMode(newIsRTL ? "Disable RTL" : "Enable RTL"); + setRTL(newIsRTL); + saveSelection("RTL", newIsRTL.toString()); + }; + + const handleSliderChange = (event: React.ChangeEvent, data: SliderOnChangeData) => { + setChartWidth(Number(data.value)); + saveSelection("ChartWidth", data.value.toString()); + }; + return (
- + Theme:   - - - -   @fluentui/react-charting  v5.23.46 - + value={value} + onOptionSelect={onOptionSelect} + > + + + +     + +   @fluentui/react-charting  v5.23.43 +
+ Chart Width:   + +
diff --git a/apps/plotly_examples/src/components/ChartWrapper.tsx b/apps/plotly_examples/src/components/ChartWrapper.tsx index 81bbdc203a..4355db64b9 100644 --- a/apps/plotly_examples/src/components/ChartWrapper.tsx +++ b/apps/plotly_examples/src/components/ChartWrapper.tsx @@ -3,14 +3,18 @@ import React from 'react'; import { paletteSlots, semanticSlots } from "../theming/v8TokenMapping"; import DeclarativeChartBasicExample from './DeclarativeChart'; -export function ChartWrapper() { +interface ChartWrapperProps { + width: number; +} + +export function ChartWrapper({ width }: ChartWrapperProps) { const v8Theme = createTheme({ palette: paletteSlots, semanticColors: semanticSlots }); //ToDo - Make the slot values dynamic return ( - -
- -
-
+ +
+ +
+
); - } +} \ No newline at end of file diff --git a/apps/plotly_examples/src/components/DeclarativeChart.tsx b/apps/plotly_examples/src/components/DeclarativeChart.tsx index 32a791a7e4..8234aeac8f 100644 --- a/apps/plotly_examples/src/components/DeclarativeChart.tsx +++ b/apps/plotly_examples/src/components/DeclarativeChart.tsx @@ -12,11 +12,40 @@ import { import { DeclarativeChart, IDeclarativeChart, Schema } from '@fluentui/react-charting'; import PlotlyChart from './PlotlyChart'; import { ErrorBoundary } from './ErrorBoundary'; -import { getSelection, saveSelection } from './utils' +import { getSelection, saveSelection } from './utils'; +import aggregatedChartTypes from './aggregated_chart_types.json'; interface IDeclarativeChartProps { } + +type PlotType = + | 'All' + | 'Area' + | 'Line' + | 'Donut' + | 'HorizontalBarWithAxis' + | 'VerticalBar' + | 'VerticalStackedBar' + | 'GroupedVerticalBar' + | 'Gauge' + | 'Pie' + | 'Sankey' + | 'Heatmap' + | 'Others'; + +type DataType = + | 'All' + | 'general' + | 'largeData' + | 'localization'; + +const dataTypeRanges = { + 'general': { min: 1, max: 252 }, + 'largeData': { min: 253, max: 277 }, + 'localization': { min: 278, max: 302 } +}; + // Use require.context to load all JSON files from the split_data folder const requireContext = require.context('../data', false, /\.json$/); const schemasData = requireContext.keys().map((fileName: string) => ({ @@ -35,6 +64,9 @@ const DeclarativeChartBasicExample: React.FC = () => { const [selectedChoice, setSelectedChoice] = React.useState(savedFileName); const [selectedSchema, setSelectedSchema] = React.useState(_selectedSchema); const [selectedLegendsState, setSelectedLegendsState] = React.useState(JSON.stringify(selectedLegends)); + const [selectedPlotTypes, setSelectedPlotTypes] = React.useState(getSelection("PlotType_filter", 'All').split(',') as PlotType[]); + const [selectedDataTypes, setSelectedDataTypes] = React.useState(getSelection("DataType_filter", 'All').split(',') as DataType[]); + const declarativeChartRef = React.useRef(null); let lastKnownValidLegends: string[] | undefined = selectedLegends; @@ -91,10 +123,90 @@ const DeclarativeChartBasicExample: React.FC = () => { setSelectedLegendsState(JSON.stringify(selectedLegends)); }; + const getFilteredData = () => { + const filteredDataItems = schemasData + .filter((data) => { + const schemaId = parseInt((data.schema as { id: string }).id, 10); + return selectedDataTypes.includes('All') || selectedDataTypes.some(dataType => { + const range = dataTypeRanges[dataType as keyof typeof dataTypeRanges]; + return schemaId >= range.min && schemaId <= range.max; + }); + }) + .filter((data) => { + const fileName = data.fileName; + const fileNumberMatch = fileName.match(/\d+/); + const fileNumber = fileNumberMatch ? fileNumberMatch[0] : '000'; + const plotType = aggregatedChartTypes[fileNumber as keyof typeof aggregatedChartTypes]; + return selectedPlotTypes.includes('All') || selectedPlotTypes.includes(plotType as PlotType); + }); + return filteredDataItems; + } + + const handleSelectPlotTypes = (_event: SelectionEvents, data: OptionOnSelectData) => { + let newSelectedPlotTypes: PlotType[]; + if (data.optionValue === 'All') { + newSelectedPlotTypes = ['All']; + } else { + newSelectedPlotTypes = selectedPlotTypes.includes(data.optionValue as PlotType) + ? selectedPlotTypes.filter(type => type !== data.optionValue) + : [...selectedPlotTypes.filter(type => type !== 'All'), data.optionValue as PlotType]; + if (newSelectedPlotTypes.length === 0) { + newSelectedPlotTypes = ['All']; + } + } + setSelectedPlotTypes(newSelectedPlotTypes as PlotType[]); + saveSelection("PlotType_filter", newSelectedPlotTypes.join(',')); + const filteredSchemas = getFilteredData(); + if (filteredSchemas.length > 0) { + const firstFilteredSchema = filteredSchemas[0]; + setSelectedChoice(firstFilteredSchema.fileName); + setSelectedSchema(firstFilteredSchema.schema); + setSelectedLegendsState(JSON.stringify((firstFilteredSchema.schema as any).selectedLegends)); + const fileNumberMatch = firstFilteredSchema.fileName.match(/\d+/); + const num_id = fileNumberMatch ? fileNumberMatch[0] : '0'; + saveSelection("Schema", num_id.toString().padStart(3, '0')); + } else { + setSelectedChoice(''); + setSelectedSchema({}); + setSelectedLegendsState(''); + } + } + + const handleSelectDataTypes = (_event: SelectionEvents, data: OptionOnSelectData) => { + let newSelectedDataTypes: DataType[]; + if (data.optionValue === 'All') { + newSelectedDataTypes = ['All']; + } else { + newSelectedDataTypes = selectedDataTypes.includes(data.optionValue as DataType) + ? selectedDataTypes.filter(type => type !== data.optionValue) + : [...selectedDataTypes.filter(type => type !== 'All'), data.optionValue as DataType]; + if (newSelectedDataTypes.length === 0) { + newSelectedDataTypes = ['All']; + } + } + setSelectedDataTypes(newSelectedDataTypes as DataType[]); + saveSelection("DataType_filter", newSelectedDataTypes.join(',')); + const filteredSchemas = getFilteredData(); + if (filteredSchemas.length > 0) { + const firstFilteredSchema = filteredSchemas[0]; + setSelectedChoice(firstFilteredSchema.fileName); + setSelectedSchema(firstFilteredSchema.schema); + setSelectedLegendsState(JSON.stringify((firstFilteredSchema.schema as any).selectedLegends)); + const fileNumberMatch = firstFilteredSchema.fileName.match(/\d+/); + const num_id = fileNumberMatch ? fileNumberMatch[0] : '0'; + saveSelection("Schema", num_id.toString().padStart(3, '0')); + } else { + setSelectedChoice(''); + setSelectedSchema({}); + setSelectedLegendsState(''); + } + } + const createDeclarativeChart = (): JSX.Element => { const theme = getSelection("Theme", "Light"); - const uniqueKey = `${selectedChoice}_${theme}`; - const plotlyKey = `plotly_${selectedChoice}_${theme}`; + const isRTL = getSelection("RTL", "false") === "true"; + const uniqueKey = `${theme}_${isRTL}`; + const plotlyKey = `plotly_${theme}_${isRTL}`; const { data, layout } = selectedSchema; if (!selectedSchema) { return
No data available
; @@ -123,11 +235,48 @@ const DeclarativeChartBasicExample: React.FC = () => { value={selectedChoice} onOptionSelect={_onChange} > - {schemasData.map((data) => ( - - ))} + {getFilteredData() + .map((data) => ( + + ))}     +     + + + + + + + + + + + + + + + +     +     + + + + + +