From 0f3f1fbf3fffaf49e25d8d0e9ecf228e39538d79 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 13 Apr 2026 22:02:31 +0200 Subject: [PATCH 01/60] Add DonutChart and HorizontalBarChart Web Component wrappers, docs and examples --- .../Charts/Examples/DonutChartDefault.razor | 18 ++++ .../Examples/DonutChartDefaultRTL.razor | 19 ++++ .../HorizontalBarChartBenchmark.razor | 40 +++++++++ .../Examples/HorizontalBarChartDefault.razor | 44 ++++++++++ .../HorizontalBarChartDefaultRTL.razor | 46 ++++++++++ .../HorizontalBarChartSingleBar.razor | 78 +++++++++++++++++ ...HorizontalBarChartSingleBarNMVariant.razor | 78 +++++++++++++++++ .../HorizontalBarChartSingleDataPoint.razor | 21 +++++ .../Components/Charts/FluentUIChartsPage.md | 18 ++++ .../Charts/Pages/FluentDonutChartPage.md | 59 +++++++++++++ .../Pages/FluentHorizontalBarChartPage.md | 85 ++++++++++++++++++ .../Components/MarkdownViewer.razor | 56 ++++++------ src/Core.Scripts/package-lock.json | 80 ++++++++++++++++- src/Core.Scripts/package.json | 4 +- src/Core.Scripts/src/FluentUIWebComponents.ts | 7 +- src/Core/Components/Charts/ChartJson.cs | 33 +++++++ .../Components/Charts/FluentDonutChart.razor | 12 +++ .../Charts/FluentDonutChart.razor.cs | 86 +++++++++++++++++++ .../Charts/FluentHorizontalBarChart.razor | 16 ++++ .../Charts/FluentHorizontalBarChart.razor.cs | 86 +++++++++++++++++++ .../DonutChartDataJsonSerializerContext.cs | 17 ++++ .../Charts/Models/DonutChartModels.cs | 66 ++++++++++++++ ...zontalBarChartDataJsonSerializerContext.cs | 18 ++++ .../Charts/Models/HorizontalBarChartModels.cs | 78 +++++++++++++++++ src/Core/Components/Tabs/FluentTabs.razor | 2 +- src/Core/Enums/HorizontalBarChartVariant.cs | 33 +++++++ ...soft.FluentUI.AspNetCore.Components.csproj | 7 +- 27 files changed, 1069 insertions(+), 38 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md create mode 100644 src/Core/Components/Charts/ChartJson.cs create mode 100644 src/Core/Components/Charts/FluentDonutChart.razor create mode 100644 src/Core/Components/Charts/FluentDonutChart.razor.cs create mode 100644 src/Core/Components/Charts/FluentHorizontalBarChart.razor create mode 100644 src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs create mode 100644 src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs create mode 100644 src/Core/Components/Charts/Models/DonutChartModels.cs create mode 100644 src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs create mode 100644 src/Core/Components/Charts/Models/HorizontalBarChartModels.cs create mode 100644 src/Core/Enums/HorizontalBarChartVariant.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor new file mode 100644 index 0000000000..e577192b29 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor @@ -0,0 +1,18 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + DonutChartData data = new DonutChartData + { + ChartTitle = "Donut chart basic example", + ChartData = new List + { + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor new file mode 100644 index 0000000000..29630b4b39 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor @@ -0,0 +1,19 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ +@code { + DonutChartData data = new DonutChartData + { + ChartTitle = "Donut chart basic example", + ChartData = new List + { + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor new file mode 100644 index 0000000000..98699cc84e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor @@ -0,0 +1,40 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + +@code { + IReadOnlyList ChartData = new List + { + new HorizontalBarChartSeries + { + ChartSeriesTitle = "one", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "one", Data = 10, Total = 100, Color = "#637cef" } + }, + BenchmarkData = 50 + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "two", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "two", Data = 30, Total = 200, Color = "#e3008c" } + }, + BenchmarkData = 30 + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "three", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "three", Data = 15, Total = 50, Color = "#2aa0a4" } + }, + BenchmarkData = 5 + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor new file mode 100644 index 0000000000..29f1acc614 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor @@ -0,0 +1,44 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" } + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" } + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" } + ] + } + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor new file mode 100644 index 0000000000..3334910cc5 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor @@ -0,0 +1,46 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + +
+ +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" } + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" } + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" } + ] + } + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor new file mode 100644 index 0000000000..004bfb53aa --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor @@ -0,0 +1,78 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + +
+ +
+ +@code { + IReadOnlyList ChartData = new List + { + new HorizontalBarChartSeries + { + ChartSeriesTitle = "one", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "two", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "three", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "four", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "five", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "six", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "seven", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "eight", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } + } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor new file mode 100644 index 0000000000..eabb0daaf7 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor @@ -0,0 +1,78 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + +
+ +
+ +@code { + IReadOnlyList ChartData = new List + { + new HorizontalBarChartSeries + { + ChartSeriesTitle = "one", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "two", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "three", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "four", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "five", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "six", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "seven", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } + } + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "eight", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } + } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor new file mode 100644 index 0000000000..d565c73b28 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor @@ -0,0 +1,21 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + +@code { + IReadOnlyList ChartData = new List + { + new HorizontalBarChartSeries + { + ChartSeriesTitle = "one", + ChartData = new List + { + new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef", Gradient = new[] { "#637cef", "#e3008c" } } + } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md new file mode 100644 index 0000000000..d070ba09d6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md @@ -0,0 +1,18 @@ +--- +title: Charts +route: /Charts/[Default] +icon: ChartMultiple +--- + +# Charts + +The Fluent UI Charts are a set of components that allow you to easily create charts in your Blazor applications. + +Currently, the following chart types are available: + +- Donut Chart +- Horizontal Bar Chart + +## Examples + +{{ DonutChartDefault }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md new file mode 100644 index 0000000000..1a034453b9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -0,0 +1,59 @@ +--- +title: Donut Chart +route: /Charts/DonutChart +--- + +# Donut Chart + +Donut charts are used to show proportion, which expresses a partial value in comparison to a total value. These types of charts are best to show percentage of individual parts in comparison to a whole, where the change over time is not important to visualize. They are circular statistical graphics divided into slices to illustrate numerical proportion. + +## Layout + +- The donut chart’s behavior is simple in application. The data is ordered from largest to smallest in clockwise direction and users can single out individual segments for clarity. +- For high cardinality scenarios where the slices are very small, they can be grouped together to form a bigger slice to improve readability. +- The chart is centered in the available screen space. The default chart diameter is 140px and bar width is 16px. This matches the width of bars in bar charts to achieve balanced scale. The size can be adjusted with responsive chart behavior, where the size of the chart and bar diameter grows proportionally in units of 4px. +- Always try to balance the visual weight of the bars in relationship to the rest of the app. +- Segments are separated by a 2px gap to maximize readability. Segment labels should be always displayed for easier chart comprehension. +- Minimum padding around the chart is 16px. It also applies to the version with labels to accommodate space for labels. There is a 2px space between the chart and the label. The label is centered in relationship to the slice it describes. That can be offset if an overlap happens between 2 labels. + +## Content + +- The donut chart consists of segments arranged clockwise from large to small. The total circle equates to 100% of the data. The segments can use custom formatting, but all values must add up to 100%. Tiny segments may be grouped and shown visually as 'Others'. +- The label string inside the donut should be concise and contain numerical information with limited or no explanation. + +## Accessibility + +- Users "Enter" into the graph and can use both arrowing and tabbing to navigate through. +- The first tab stop will stop on the graph and give a description of what type of graph it is. +- Each segment can define its own accessibility label to help the user understand the data better. + +## Do's + +- For scenarios with lots of categories, consider changing the type of graph to a stacked horizontal bar chart. +- We recommend donut charts over pie charts as they are more readable. + +## Dont's + +- Don't overuse donuts charts. They require a lot of space on the page and using more than one next to each other dilutes the intended message. + +## Examples + +### Default Donut Chart + +{{ DonutChartDefault }} + +### RTL Donut Chart + +{{ DonutChartDefaultRTL }} + +## API Fluent Donut Chart + +{{ API Type=FluentDonutChart }} + +## API Donut Chart Data + +{{ API Type=DonutChartData Properties=All }} + +## API Donut Chart Data Point + +{{ API Type=DonutChartDataPoint Properties=All }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md new file mode 100644 index 0000000000..11174e0c32 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md @@ -0,0 +1,85 @@ +--- +title: Horizontal Bar Chart +route: /Charts/HorizontalBarChart +--- + +# Horizontal Bar Chart + +A horizontal bar chart is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent. This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long. + +## Layout + +Use a horizontal bar graph to compare between different values that are hierarchically equivalent. The rectangular bar length is proportional to the values they represent. There will always be a maximum data value (color) representing the total length. + +Horizontal bar chart can be of 2 types - + +- Absolute scale the length of the bar is proportional to the biggest value for the category. +- n/M scale the length of the bar is determined by the total/target value of the specific bar. As a result, 2 adjacent bars can have different data scales and not be comparable. This aspect should be kept in mind while using this chart type. See HorizontalBarChart benchmark example to see the behavior. Each bar has a different scale - 100, 200 and 50 units. + +## Content + +- Title/Label The label for the bar. It is displayed above the bar and can represent longer texts. +- Bar segment The bar segment represents the current value of the category. For n/M variant there is a placeholder segment to show the left-over values. +- Bar value The value of the bar is represented on the right side. This can be absolute or percentage format. This can also be in fractional form representing current value out of total value. See the chartDataMode property to use it. +- Benchmark The benchmark value is shown as an inverted triangle in the chart. + +## Accessibility + +- Bar graphs should be flexible to their containers. They will change widths to fit their environment. +- Each section of the bar chart is readable via screen readers. The user can navigate through the entire bar graph by using the tab keys. +- The chart reflows to accommodate zooming in to 400%. + +## Customizing the chart + +- Bar chart custom data This property allows customizing the right-side data part of the chart. See the usage of barChartCustomData prop in custom callout variant. +- Custom hover callout See onRenderCalloutPerHorizontalBar prop to customize the hover callout. Set the chartDataMode as number, fraction or percentage to specify how numerical values will be shown on the chart. +- Benchmark data Set the data attribute of IChartDataPoint to specify the benchmark value. The benchmark value is shown as an inverted triangle in the chart. +- AbsoluteScale variant The bar labels are shown by default in the absolute-scale variant. Set the hideLabels prop to hide them. + +## Do's + +- Use horizontal bar chart if the length of labels is longer. +- Numerical units on labels are represented through abbreviations. + +## Don'ts + +- Avoid having more than 20 bars in the chart. +- The n/M variant should be used only when a value has to be compared against its target value. + +## Examples + +### Default horizontal bar chart + +{{ HorizontalBarChartDefault }} + +### Single Bar + +{{ HorizontalBarChartSingleBar }} + +### Single Bar NM Variant + +{{ HorizontalBarChartSingleBarNMVariant }} + +### Benchmark + +{{ HorizontalBarChartBenchmark }} + +### Single Data Point + +{{ HorizontalBarChartSingleDataPoint }} + +### RTL + +{{ HorizontalBarChartDefaultRTL }} + +## API Fluent Horizontal Bar Chart + +{{ API Type=FluentHorizontalBarChart }} + +## API Horizontal Bar Chart Series + +{{ API Type=HorizontalBarChartSeries Properties=All }} + +## API Horizontal Bar Chart Data Point + +{{ API Type=HorizontalBarChartDataPoint Properties=All }} diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor index 180fae5690..398d5d1944 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor @@ -44,40 +44,46 @@ } else if (section.ExtraFiles.Count > 0) { + var id = $"f{section.Id}";
- - Example + + Example @foreach (var tabName in section.ExtraFiles.Keys) { - @tabName + @tabName } - - - - - @foreach (var item in section.ExtraFiles) - { - -
Loading...
-
- } -
+ + +
+ +
+ @foreach (var item in section.ExtraFiles) + { +
+
Loading...
+
+ }
} else { + var id = $"f{section.Id}";
- - Example - Code - - - - - -
Loading...
-
-
+ + + Example + + + Code + + + +
+ +
+
+
Loading...
+
} diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json index 0d46ec4164..984874ba73 100644 --- a/src/Core.Scripts/package-lock.json +++ b/src/Core.Scripts/package-lock.json @@ -7,7 +7,8 @@ "name": "microsoft.fluentui.aspnetcore.components.assets", "license": "ISC", "dependencies": { - "@fluentui/web-components": "^3.0.0-rc.9" + "@fluentui/chart-web-components": "0.0.71", + "@fluentui/web-components": "^3.0.0-rc.13" }, "devDependencies": { "@types/sortablejs": "^1.15.9", @@ -572,6 +573,26 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@fluentui/chart-web-components": { + "version": "0.0.71", + "resolved": "https://registry.npmjs.org/@fluentui/chart-web-components/-/chart-web-components-0.0.71.tgz", + "integrity": "sha512-XJ6XcpOyhnpWgUpSNIh32EHV1sGEF98TAdWJ4x7ZoQpmugYY1yGMRsYCAVCuxMl4r0apQstcySdbJnrtdDY2Kw==", + "license": "MIT", + "dependencies": { + "@fluentui/tokens": "^1.0.0-alpha.23", + "@fluentui/web-components": "^3.0.0-rc.13", + "@microsoft/fast-web-utilities": "^6.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "tabbable": "^6.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" + } + }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", @@ -582,9 +603,9 @@ } }, "node_modules/@fluentui/web-components": { - "version": "3.0.0-rc.9", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/web-components/-/web-components-3.0.0-rc.9.tgz", - "integrity": "sha1-Os43u2jylM3eWblTiSX5hRhY2JE=", + "version": "3.0.0-rc.13", + "resolved": "https://registry.npmjs.org/@fluentui/web-components/-/web-components-3.0.0-rc.13.tgz", + "integrity": "sha512-j/AKW81cpUqGuoKTdlCY8BHB9IEi/mf+ERNCxpLR9772XAUHlLHchqQCCuHSoNC/HZrc4FeW2UxEUUJWXTvyPA==", "license": "MIT", "dependencies": { "@fluentui/tokens": "1.0.0-alpha.23", @@ -676,6 +697,27 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -1039,6 +1081,36 @@ "node": ">= 8" } }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/debug/-/debug-4.4.3.tgz", diff --git a/src/Core.Scripts/package.json b/src/Core.Scripts/package.json index 0db41e81dd..0e1818896a 100644 --- a/src/Core.Scripts/package.json +++ b/src/Core.Scripts/package.json @@ -27,6 +27,8 @@ "typescript": "5.9.3" }, "dependencies": { - "@fluentui/web-components": "^3.0.0-rc.9" + "@fluentui/web-components": "^3.0.0-rc.13", + "@fluentui/chart-web-components": "0.0.71" + } } diff --git a/src/Core.Scripts/src/FluentUIWebComponents.ts b/src/Core.Scripts/src/FluentUIWebComponents.ts index 762e8fbe19..4b32a31e41 100644 --- a/src/Core.Scripts/src/FluentUIWebComponents.ts +++ b/src/Core.Scripts/src/FluentUIWebComponents.ts @@ -1,4 +1,5 @@ import * as FluentUIComponents from '@fluentui/web-components' +import * as FluentUICharts from '@fluentui/chart-web-components' export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { @@ -45,8 +46,6 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { FluentUIComponents.SwitchDefinition.define(registry); FluentUIComponents.TabDefinition.define(registry); FluentUIComponents.TablistDefinition.define(registry); - FluentUIComponents.TabPanelDefinition.define(registry); - FluentUIComponents.TabsDefinition.define(registry); FluentUIComponents.TextAreaDefinition.define(registry); FluentUIComponents.TextDefinition.define(registry); FluentUIComponents.TextInputDefinition.define(registry); @@ -54,5 +53,9 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { FluentUIComponents.TooltipDefinition.define(registry); FluentUIComponents.TreeDefinition.define(registry); FluentUIComponents.TreeItemDefinition.define(registry); + + // Register Chart Web Components + FluentUICharts.DonutChartDefinition.define(registry); + FluentUICharts.HorizontalBarChartDefinition.define(registry); } } diff --git a/src/Core/Components/Charts/ChartJson.cs b/src/Core/Components/Charts/ChartJson.cs new file mode 100644 index 0000000000..2a3d4d0d3f --- /dev/null +++ b/src/Core/Components/Charts/ChartJson.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Provides shared source-generated JSON serialization helpers for chart payloads. +/// +public static class ChartJson +{ + /// + /// Serializes donut chart data using the donut chart serializer context. + /// + /// The donut chart data payload. + /// A JSON string suitable for the fluent-donut-chart component. + public static string Serialize(DonutChartData value) => + JsonSerializer.Serialize( + value, + DonutChartDataJsonSerializerContext.Default.DonutChartData); + + /// + /// Serializes horizontal bar chart data using the horizontal bar chart serializer context. + /// + /// The horizontal bar chart series collection. + /// A JSON string suitable for the fluent-horizontal-bar-chart component. + public static string Serialize(IReadOnlyList value) => + JsonSerializer.Serialize( + value, + HorizontalBarChartDataJsonSerializerContext.Default.IReadOnlyListHorizontalBarChartSeries); +} diff --git a/src/Core/Components/Charts/FluentDonutChart.razor b/src/Core/Components/Charts/FluentDonutChart.razor new file mode 100644 index 0000000000..8e3a649631 --- /dev/null +++ b/src/Core/Components/Charts/FluentDonutChart.razor @@ -0,0 +1,12 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components.Charts +@inherits FluentComponentBase + + + diff --git a/src/Core/Components/Charts/FluentDonutChart.razor.cs b/src/Core/Components/Charts/FluentDonutChart.razor.cs new file mode 100644 index 0000000000..40193375ec --- /dev/null +++ b/src/Core/Components/Charts/FluentDonutChart.razor.cs @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// A FluentDonutChart is a component that displays data in a donut chart format. +/// +public partial class FluentDonutChart : FluentComponentBase +{ + + /// + public FluentDonutChart(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + /// + internal string? ClassValue => DefaultClassBuilder + .AddClass("fluent-donut-chart") + .Build(); + + /// + internal string? StyleValue => DefaultStyleBuilder + .AddStyle("width", Width.HasValue ? $"{Width.Value}px" : null, when: Width.HasValue) + .AddStyle("height", Height.HasValue ? $"{Height.Value}px" : null, when: Height.HasValue) + .Build(); + + internal string SerializedData => JsonSerializer.Serialize(ChartData, typeof(DonutChartData), DonutChartDataJsonSerializerContext.Default); + + /// + /// Gets or sets the data for the donut chart. + /// + [Parameter, EditorRequired] + public DonutChartData ChartData { get; set; } = new(); + + /// + /// Gets or sets the height of the donut chart. + /// + [Parameter] + public int? Height { get; set; } + + /// + /// Gets or sets the width of the donut chart. + /// + [Parameter] + public int? Width { get; set; } + + /// + /// Gets or sets a value indicating whether legends are hidden in the component output. + /// + [Parameter] + public bool HideLegends { get; set; } + + /// + /// Gets or sets a value indicating whether the tooltip is hidden. + /// + [Parameter] + public bool HideTooltip { get; set; } + + /// + /// Gets or sets the inner radius of the component, in pixels. + /// + /// If , a default inner radius is used. The value must be non-negative. + /// This property is typically used to control the thickness of ring-shaped visual elements. + [Parameter] + public int? InnerRadius { get; set; } + + /// + /// Gets or sets the value displayed inside the donut hole. This is typically used to show a summary + /// or total value related to the data represented by the chart. + /// + [Parameter] + public string? ValueInsideDonut { get; set; } + + /// + /// Gets or sets the label text displayed for the legend list. + /// + [Parameter] + public string? LegendListLabel { get; set; } +} diff --git a/src/Core/Components/Charts/FluentHorizontalBarChart.razor b/src/Core/Components/Charts/FluentHorizontalBarChart.razor new file mode 100644 index 0000000000..0b5c14c3b1 --- /dev/null +++ b/src/Core/Components/Charts/FluentHorizontalBarChart.razor @@ -0,0 +1,16 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Extensions +@inherits FluentComponentBase + + + diff --git a/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs b/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs new file mode 100644 index 0000000000..c4ea80dc93 --- /dev/null +++ b/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Enums; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// A FluentHorizontalBarChart is a component that displays data in a horizontal bar chart format. +/// +public partial class FluentHorizontalBarChart : FluentComponentBase +{ + /// + public FluentHorizontalBarChart(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + // + internal string? ClassValue => DefaultClassBuilder + .AddClass("fluent-horizontal-bar-chart") + .Build(); + + // + internal string? StyleValue => DefaultStyleBuilder + .Build(); + + internal string SerializedData => JsonSerializer.Serialize(ChartData, typeof(HorizontalBarChartSeries), HorizontalBarChartDataJsonSerializerContext.Default); + + /// + /// Gets or sets the data for the horizontal bar chart. + /// + [Parameter, EditorRequired] + public IReadOnlyList? ChartData { get; set; } + + /// + /// Gets or sets the visual variant to use for rendering + /// the horizontal bar chart. + /// + /// + /// Specify this property to control the appearance or style of the chart. + /// If not set, the default variant is used. + /// + [Parameter] + public HorizontalBarChartVariant? Variant { get; set; } + + /// + /// Gets or sets a value indicating whether the ratio is hidden in the component output. + /// + [Parameter] + public bool HideRatio { get; set; } + + /// + /// Gets or sets a value indicating whether legends are hidden in the component output. + /// + [Parameter] + public bool HideLegends { get; set; } + + /// + /// Gets or sets a value indicating whether the tooltip is hidden. + /// + [Parameter] + public bool HideTooltip { get; set; } + + /// + /// Gets or sets the label displayed for the legend list. + /// + [Parameter] + public string? LegendListLabel { get; set; } + + /// + /// Gets or sets the title text displayed on the chart. + /// + [Parameter] + public string? ChartTitle { get; set; } + + /// + /// Gets a value indicating whether the component has data that can be rendered safely. + /// + protected bool HasRenderableData => + ChartData is { Count: > 0 }; +} diff --git a/src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs b/src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs new file mode 100644 index 0000000000..95b01a8f94 --- /dev/null +++ b/src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Provides source-generated JSON serialization metadata for donut chart payloads. +/// +[JsonSerializable(typeof(DonutChartData))] +[JsonSerializable(typeof(DonutChartDataPoint))] +[JsonSerializable(typeof(IReadOnlyList))] +internal sealed partial class DonutChartDataJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Core/Components/Charts/Models/DonutChartModels.cs b/src/Core/Components/Charts/Models/DonutChartModels.cs new file mode 100644 index 0000000000..e902a12b43 --- /dev/null +++ b/src/Core/Components/Charts/Models/DonutChartModels.cs @@ -0,0 +1,66 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +#pragma warning disable MA0048 // File name must match type name + +/// +/// Represents a single data point in a donut chart. +/// +public sealed record DonutChartDataPoint +{ + /// + /// Gets the legend text shown for the donut segment. + /// + [JsonPropertyName("legend")] + public string Legend { get; init; } = string.Empty; + + /// + /// Gets the numeric value of the donut segment. + /// + [JsonPropertyName("data")] + public double Data { get; init; } + + /// + /// Gets the color used to render the donut segment and legend. + /// If not provided, the web component falls back to its default palette. + /// + [JsonPropertyName("color")] + public string? Color { get; init; } + + /// + /// Gets optional callout data for the x-axis portion of the tooltip. + /// + [JsonPropertyName("xAxisCalloutData")] + public string? XAxisCalloutData { get; init; } + + /// + /// Gets optional callout data for the y-axis portion of the tooltip. + /// If not provided, the component may fall back to the numeric data value. + /// + [JsonPropertyName("yAxisCalloutData")] + public string? YAxisCalloutData { get; init; } +} + +/// +/// Represents the full data payload consumed by the donut chart web component. +/// +public sealed record DonutChartData +{ + /// + /// Gets the optional title of the chart. + /// + [JsonPropertyName("chartTitle")] + public string? ChartTitle { get; init; } + + /// + /// Gets the collection of donut chart segments. + /// + [JsonPropertyName("chartData")] + public IReadOnlyList ChartData { get; init; } = []; +} +#pragma warning restore MA0048 // File name must match type name diff --git a/src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs b/src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs new file mode 100644 index 0000000000..faeaa3d5b2 --- /dev/null +++ b/src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs @@ -0,0 +1,18 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Provides source-generated JSON serialization metadata for horizontal bar chart payloads. +/// +[JsonSerializable(typeof(HorizontalBarChartSeries))] +[JsonSerializable(typeof(HorizontalBarChartDataPoint))] +[JsonSerializable(typeof(IReadOnlyList))] +[JsonSerializable(typeof(IReadOnlyList))] +internal sealed partial class HorizontalBarChartDataJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Core/Components/Charts/Models/HorizontalBarChartModels.cs b/src/Core/Components/Charts/Models/HorizontalBarChartModels.cs new file mode 100644 index 0000000000..98b7e34a28 --- /dev/null +++ b/src/Core/Components/Charts/Models/HorizontalBarChartModels.cs @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +#pragma warning disable MA0048 // File name must match type name + +/// +/// Represents a single data point in a horizontal bar chart series. +/// +public sealed record HorizontalBarChartDataPoint +{ + /// + /// Gets the legend text shown for the bar segment. + /// + [JsonPropertyName("legend")] + public string Legend { get; init; } = string.Empty; + + /// + /// Gets the numeric value represented by the bar segment. + /// + [JsonPropertyName("data")] + public double Data { get; init; } + + /// + /// Gets the optional total bar length used for ratio-style rendering. + /// + [JsonPropertyName("total")] + public double? Total { get; init; } + + /// + /// Gets the solid color used to render the bar segment. + /// If not provided, the component may fall back to its default palette behavior. + /// + [JsonPropertyName("color")] + public string? Color { get; init; } + + /// + /// Gets the optional two-color gradient used to render the bar segment. + /// The array should contain exactly two color values: start and end. + /// + [JsonPropertyName("gradient")] + public string[]? Gradient { get; init; } +} + +/// +/// Represents one horizontal bar chart series in the data payload. +/// +public sealed record HorizontalBarChartSeries +{ + /// + /// Gets the optional title shown for the data series. + /// + [JsonPropertyName("chartSeriesTitle")] + public string? ChartSeriesTitle { get; init; } + + /// + /// Gets the collection of data points rendered within the series. + /// + [JsonPropertyName("chartData")] + public IReadOnlyList ChartData { get; init; } = []; + + /// + /// Gets the optional benchmark value used to render the benchmark indicator. + /// + [JsonPropertyName("benchmarkData")] + public double? BenchmarkData { get; init; } + + /// + /// Gets optional text displayed alongside the chart data for the series. + /// + [JsonPropertyName("chartDataText")] + public string? ChartDataText { get; init; } +} +#pragma warning restore MA0048 // File name must match type name diff --git a/src/Core/Components/Tabs/FluentTabs.razor b/src/Core/Components/Tabs/FluentTabs.razor index 29493ba0c0..7c2b9eb4af 100644 --- a/src/Core/Components/Tabs/FluentTabs.razor +++ b/src/Core/Components/Tabs/FluentTabs.razor @@ -21,7 +21,7 @@ { @if (tab.Visible) { -
+
@if (tab.DeferredLoading && ActiveTabId != tab.Id) { if (tab.LoadingTemplate is null) diff --git a/src/Core/Enums/HorizontalBarChartVariant.cs b/src/Core/Enums/HorizontalBarChartVariant.cs new file mode 100644 index 0000000000..b441772ddf --- /dev/null +++ b/src/Core/Enums/HorizontalBarChartVariant.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; + +namespace Microsoft.FluentUI.AspNetCore.Components.Enums; + +/// +/// +/// +public enum HorizontalBarChartVariant +{ + /// + /// Specifies that the data represents a part-to-whole relationship, where each value is a component of a total. + /// + /// Use this value when visualizing or analyzing data in which individual elements contribute to + /// a collective whole, such as in pie charts or stacked bar charts. + [Description("part-to-whole")] + PartToWhole, + + /// + /// Gets or sets the absolute scaling factor applied to the object. + /// + [Description("absolute-scale")] + AbsoluteScale, + + /// + /// Represents a single bar element, typically used in charting or data visualization scenarios. + /// + [Description("single-bar")] + SingleBar, +} diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj index 81fbacf1af..af72772611 100644 --- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj +++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj @@ -139,12 +139,9 @@ - + - <_EsprojAssetsToFix Include="@(StaticWebAsset)" - Condition="'%(StaticWebAsset.OriginalItemSpec)' != '' AND $([System.String]::Copy('%(StaticWebAsset.OriginalItemSpec)').EndsWith('.esproj'))" /> + <_EsprojAssetsToFix Include="@(StaticWebAsset)" Condition="'%(StaticWebAsset.OriginalItemSpec)' != '' AND $([System.String]::Copy('%(StaticWebAsset.OriginalItemSpec)').EndsWith('.esproj'))" /> From 7d5edce15425f4e8505c3ad48cec0b4597cdbdf6 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 14 Apr 2026 09:14:19 +0200 Subject: [PATCH 02/60] Process review comments --- .../Components/Charts/Pages/FluentDonutChartPage.md | 2 +- src/Core/Components/Charts/FluentDonutChart.razor.cs | 3 --- .../Components/Charts/FluentHorizontalBarChart.razor.cs | 9 +++------ src/Core/Enums/HorizontalBarChartVariant.cs | 6 +++++- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md index 1a034453b9..9fcbabff6f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -32,7 +32,7 @@ Donut charts are used to show proportion, which expresses a partial value in com - For scenarios with lots of categories, consider changing the type of graph to a stacked horizontal bar chart. - We recommend donut charts over pie charts as they are more readable. -## Dont's +## Don'ts - Don't overuse donuts charts. They require a lot of space on the page and using more than one next to each other dilutes the intended message. diff --git a/src/Core/Components/Charts/FluentDonutChart.razor.cs b/src/Core/Components/Charts/FluentDonutChart.razor.cs index 40193375ec..e192a92a62 100644 --- a/src/Core/Components/Charts/FluentDonutChart.razor.cs +++ b/src/Core/Components/Charts/FluentDonutChart.razor.cs @@ -2,7 +2,6 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using System.Text.Json; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Utilities; @@ -31,8 +30,6 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration .AddStyle("height", Height.HasValue ? $"{Height.Value}px" : null, when: Height.HasValue) .Build(); - internal string SerializedData => JsonSerializer.Serialize(ChartData, typeof(DonutChartData), DonutChartDataJsonSerializerContext.Default); - /// /// Gets or sets the data for the donut chart. /// diff --git a/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs b/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs index c4ea80dc93..c841a42220 100644 --- a/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs +++ b/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs @@ -2,7 +2,6 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using System.Text.Json; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Enums; using Microsoft.FluentUI.AspNetCore.Components.Utilities; @@ -20,22 +19,20 @@ public FluentHorizontalBarChart(LibraryConfiguration configuration) : base(confi Id = Identifier.NewId(); } - // + /// internal string? ClassValue => DefaultClassBuilder .AddClass("fluent-horizontal-bar-chart") .Build(); - // + /// internal string? StyleValue => DefaultStyleBuilder .Build(); - internal string SerializedData => JsonSerializer.Serialize(ChartData, typeof(HorizontalBarChartSeries), HorizontalBarChartDataJsonSerializerContext.Default); - /// /// Gets or sets the data for the horizontal bar chart. /// [Parameter, EditorRequired] - public IReadOnlyList? ChartData { get; set; } + public IReadOnlyList ChartData { get; set; } = []; /// /// Gets or sets the visual variant to use for rendering diff --git a/src/Core/Enums/HorizontalBarChartVariant.cs b/src/Core/Enums/HorizontalBarChartVariant.cs index b441772ddf..6333fb4c1a 100644 --- a/src/Core/Enums/HorizontalBarChartVariant.cs +++ b/src/Core/Enums/HorizontalBarChartVariant.cs @@ -20,7 +20,11 @@ public enum HorizontalBarChartVariant PartToWhole, /// - /// Gets or sets the absolute scaling factor applied to the object. + /// Specifies that bar lengths are based on the raw data values using a + /// shared absolute scale. Choose this variant when comparing magnitudes + /// across bars, rather than showing each bar as part of a total as in + /// , or rendering a standalone value as in + /// . /// [Description("absolute-scale")] AbsoluteScale, From 8cf21bafeba2e148a361470680f7c66a61c1164c Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 14 Apr 2026 09:41:31 +0200 Subject: [PATCH 03/60] More review comments processed --- src/Core/Components/Charts/FluentDonutChart.razor | 8 ++++++-- src/Core/Enums/HorizontalBarChartVariant.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Core/Components/Charts/FluentDonutChart.razor b/src/Core/Components/Charts/FluentDonutChart.razor index 8e3a649631..99278f2331 100644 --- a/src/Core/Components/Charts/FluentDonutChart.razor +++ b/src/Core/Components/Charts/FluentDonutChart.razor @@ -1,12 +1,16 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts @inherits FluentComponentBase - + inner-radius="@InnerRadius" + @attributes="@AdditionalAttributes"> diff --git a/src/Core/Enums/HorizontalBarChartVariant.cs b/src/Core/Enums/HorizontalBarChartVariant.cs index 6333fb4c1a..6f1e5cee1e 100644 --- a/src/Core/Enums/HorizontalBarChartVariant.cs +++ b/src/Core/Enums/HorizontalBarChartVariant.cs @@ -7,7 +7,7 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Enums; /// -/// +/// Specifies which visual variant the HorizontalBarChart uses. /// public enum HorizontalBarChartVariant { From d4cb02cc688b0551e31aa00b44af5b108245daec Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 16 Apr 2026 14:01:25 +0200 Subject: [PATCH 04/60] Introduce separate Charts package. This uses a new Charts.Scripts package so there isno chart related code in the core scripts anymore. --- .gitignore | 1 + Microsoft.FluentUI-v5.slnx | 7 + .../FluentUI.Demo.Client.csproj | 1 + .../Demo/FluentUI.Demo/FluentUI.Demo.csproj | 1 + src/Charts.Scripts/.eslintrc.json | 12 + src/Charts.Scripts/.npmrc | 3 + ...spNetCore.Components.Charts.Scripts.esproj | 13 + src/Charts.Scripts/esbuild.config.mjs | 46 + src/Charts.Scripts/package-lock.json | 2075 +++++++++++++++++ src/Charts.Scripts/package.json | 34 + src/Charts.Scripts/src/BuildConstants.ts | 2 + .../src/FluentUIChartComponents.ts | 25 + src/Charts.Scripts/src/index.ts | 9 + src/Charts.Scripts/tsconfig.json | 33 + src/{Core/Components => }/Charts/ChartJson.cs | 0 .../Charts/DonutChart}/FluentDonutChart.razor | 0 .../DonutChart}/FluentDonutChart.razor.cs | 1 + .../FluentHorizontalBarChart.razor | 0 .../FluentHorizontalBarChart.razor.cs | 0 .../Enums/HorizontalBarChartVariant.cs | 0 src/Charts/GlobalSuppressions.cs | 8 + ...uentUI.AspNetCore.Components.Charts.csproj | 173 ++ .../DonutChartDataJsonSerializerContext.cs | 0 .../Charts/Models/DonutChartModels.cs | 0 ...zontalBarChartDataJsonSerializerContext.cs | 0 .../Charts/Models/HorizontalBarChartModels.cs | 0 src/Charts/README.md | 1 + src/Charts/_Imports.razor | 9 + src/Core.Scripts/.npmrc | 3 +- ...entUI.AspNetCore.Components.Scripts.esproj | 2 +- src/Core.Scripts/package-lock.json | 130 +- src/Core.Scripts/package.json | 8 +- src/Core.Scripts/src/FluentUIWebComponents.ts | 175 +- src/Core.Scripts/src/RegistrationState.ts | 28 + ...soft.FluentUI.AspNetCore.Components.csproj | 14 +- 35 files changed, 2652 insertions(+), 162 deletions(-) create mode 100644 src/Charts.Scripts/.eslintrc.json create mode 100644 src/Charts.Scripts/.npmrc create mode 100644 src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj create mode 100644 src/Charts.Scripts/esbuild.config.mjs create mode 100644 src/Charts.Scripts/package-lock.json create mode 100644 src/Charts.Scripts/package.json create mode 100644 src/Charts.Scripts/src/BuildConstants.ts create mode 100644 src/Charts.Scripts/src/FluentUIChartComponents.ts create mode 100644 src/Charts.Scripts/src/index.ts create mode 100644 src/Charts.Scripts/tsconfig.json rename src/{Core/Components => }/Charts/ChartJson.cs (100%) rename src/{Core/Components/Charts => Charts/Charts/DonutChart}/FluentDonutChart.razor (100%) rename src/{Core/Components/Charts => Charts/Charts/DonutChart}/FluentDonutChart.razor.cs (97%) rename src/{Core/Components/Charts => Charts/Charts/HorizontalBarChart}/FluentHorizontalBarChart.razor (100%) rename src/{Core/Components/Charts => Charts/Charts/HorizontalBarChart}/FluentHorizontalBarChart.razor.cs (100%) rename src/{Core => Charts}/Enums/HorizontalBarChartVariant.cs (100%) create mode 100644 src/Charts/GlobalSuppressions.cs create mode 100644 src/Charts/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj rename src/{Core/Components => }/Charts/Models/DonutChartDataJsonSerializerContext.cs (100%) rename src/{Core/Components => }/Charts/Models/DonutChartModels.cs (100%) rename src/{Core/Components => }/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs (100%) rename src/{Core/Components => }/Charts/Models/HorizontalBarChartModels.cs (100%) create mode 100644 src/Charts/README.md create mode 100644 src/Charts/_Imports.razor create mode 100644 src/Core.Scripts/src/RegistrationState.ts diff --git a/.gitignore b/.gitignore index 61f81fb7a7..8ec35f9708 100644 --- a/.gitignore +++ b/.gitignore @@ -416,6 +416,7 @@ FodyWeavers.xsd *.cobertura.xml Microsoft.FluentUI.AspNetCore.Components.xml Microsoft.FluentUI.AspNetCore.McpServer.xml +Microsoft.FluentUI.AspNetCore.Components.Charts.xml /examples/Demo/FluentUI.Demo.Client/wwwroot/sources/ /examples/Demo/FluentUI.Demo.Client/wwwroot/documentation/ api-comments.json diff --git a/Microsoft.FluentUI-v5.slnx b/Microsoft.FluentUI-v5.slnx index 224d81cb29..3a80d94400 100644 --- a/Microsoft.FluentUI-v5.slnx +++ b/Microsoft.FluentUI-v5.slnx @@ -20,6 +20,13 @@ + + + + + + + diff --git a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj index 13f947b172..06dba6a381 100644 --- a/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj +++ b/examples/Demo/FluentUI.Demo.Client/FluentUI.Demo.Client.csproj @@ -45,6 +45,7 @@ + diff --git a/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj b/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj index 9e621ce7ed..99b6f80e36 100644 --- a/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj +++ b/examples/Demo/FluentUI.Demo/FluentUI.Demo.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Charts.Scripts/.eslintrc.json b/src/Charts.Scripts/.eslintrc.json new file mode 100644 index 0000000000..c45c6edbaa --- /dev/null +++ b/src/Charts.Scripts/.eslintrc.json @@ -0,0 +1,12 @@ +{ + "parser": "@typescript-eslint/parser", + "parserOptions": { + "ecmaVersion": "latest", + "sourceType": "module" + }, + "plugins": [ "@typescript-eslint" ], + "root": true, + "rules": { + // Add rules here + } +} \ No newline at end of file diff --git a/src/Charts.Scripts/.npmrc b/src/Charts.Scripts/.npmrc new file mode 100644 index 0000000000..642288e0b9 --- /dev/null +++ b/src/Charts.Scripts/.npmrc @@ -0,0 +1,3 @@ +registry=http://localhost:4873/ +always-auth=true + diff --git a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj new file mode 100644 index 0000000000..d4efe8680a --- /dev/null +++ b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj @@ -0,0 +1,13 @@ + + + dist\ + Microsoft.FluentUI.AspNetCore.Components.Charts + false + false + npm run build -- --build-mode=$(Configuration) + npm run clean + + + + + \ No newline at end of file diff --git a/src/Charts.Scripts/esbuild.config.mjs b/src/Charts.Scripts/esbuild.config.mjs new file mode 100644 index 0000000000..7cf1a31ed8 --- /dev/null +++ b/src/Charts.Scripts/esbuild.config.mjs @@ -0,0 +1,46 @@ +import * as esbuild from 'esbuild' +import { glob } from 'glob' +import { writeFile, unlink } from 'fs/promises'; +import pkg from './package.json' with { type: 'json' } +import fs from "fs"; +import path from "path"; + +// Generate BuildConstants.ts with BUILD_MODE constant based on --build-mode argument +const rawBuildMode = process.argv.find(a => a.startsWith('--build-mode='))?.split('=')[1]; +const buildMode = rawBuildMode === "Release" ? "Release" : "Debug"; + +const constantsFile = path.resolve("src/BuildConstants.ts"); +const constantsContent = `// This file is generated by a tool. Do not change!\nexport const BUILD_MODE = '${buildMode}';\n`; +const isNotCurrentMode = !fs.existsSync(constantsFile) || !fs.readFileSync(constantsFile, 'utf-8').includes(`BUILD_MODE = '${buildMode}'`); +if (isNotCurrentMode) { + fs.writeFileSync(constantsFile, constantsContent); +} + +// JS: Microsoft.FluentUI.AspNetCore.Components.Charts.lib.module.js +await esbuild.build({ + entryPoints: [pkg.source], + bundle: true, + minify: buildMode !== "Debug", + sourcemap: buildMode === "Debug", + logLevel: 'info', + target: 'es2022', + format: 'esm', + outfile: pkg.main, + legalComments: 'none' +}); + +// CSS: Microsoft.FluentUI.AspNetCore.Components.Charts.bundle.scp.css +const allStyleFile = 'all-styles.css'; +const files = await glob(pkg.cssFiles); +const content = files.map(file => `@import "${file.replace(/\\/g, '/')}";`).join('\n'); + +await writeFile(allStyleFile, content); +await esbuild.build({ + entryPoints: [allStyleFile], + loader: { '.css': 'css' }, + outfile: pkg.cssBundle, + bundle: true, + minify: true, + sourcemap: false, +}); +await unlink(allStyleFile); diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json new file mode 100644 index 0000000000..1077d72c83 --- /dev/null +++ b/src/Charts.Scripts/package-lock.json @@ -0,0 +1,2075 @@ +{ + "name": "microsoft.fluentui.aspnetcore.components.charts.assets", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "microsoft.fluentui.aspnetcore.components.charts.assets", + "license": "ISC", + "dependencies": { + "@fluentui/chart-web-components": "0.0.72", + "@fluentui/web-components": "^3.0.0-rc.13" + }, + "devDependencies": { + "@types/sortablejs": "^1.15.9", + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "esbuild": "0.27.4", + "esbuild-plugin-inline-css": "0.0.1", + "eslint": "10.2.0", + "glob": "^13.0.6", + "imask": "^7.6.1", + "rimraf": "6.1.3", + "tabbable": "6.4.0", + "typescript": "5.9.3" + } + }, + "node_modules/@babel/runtime-corejs3": { + "version": "7.29.2", + "resolved": "http://localhost:4873/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-js-pure": "^3.48.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", + "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/android-arm/-/android-arm-0.27.4.tgz", + "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", + "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/android-x64/-/android-x64-0.27.4.tgz", + "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", + "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", + "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", + "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", + "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", + "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", + "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", + "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", + "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", + "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", + "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", + "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", + "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", + "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", + "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", + "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", + "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", + "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", + "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", + "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", + "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", + "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.4", + "resolved": "http://localhost:4873/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", + "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@fluentui/chart-web-components": { + "version": "0.0.72", + "resolved": "http://localhost:4873/@fluentui/chart-web-components/-/chart-web-components-0.0.72.tgz", + "integrity": "sha512-W90OP0ckOOAmTvSDI7U2jORsMUdW6KFfVu1alI3Ccr0bDQGoTZFxIkvIpq18zzWLgdpYKtHBwxdfA62k+QgF2A==", + "license": "MIT", + "dependencies": { + "@fluentui/tokens": "^1.0.0-alpha.23", + "@fluentui/web-components": "3.0.0-rc.13", + "@microsoft/fast-web-utilities": "^6.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", + "d3-format": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "tabbable": "^6.2.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" + } + }, + "node_modules/@fluentui/tokens": { + "version": "1.0.0-alpha.23", + "resolved": "http://localhost:4873/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", + "integrity": "sha1-T4RsHk/Ns8qA6zGALEo2bVWZsw4=", + "license": "MIT", + "dependencies": { + "@swc/helpers": "^0.5.1" + } + }, + "node_modules/@fluentui/web-components": { + "version": "3.0.0-rc.13", + "resolved": "http://localhost:4873/@fluentui/web-components/-/web-components-3.0.0-rc.13.tgz", + "integrity": "sha1-ATuXKxB4p/FDESanYDaRZzwnI9w=", + "license": "MIT", + "dependencies": { + "@fluentui/tokens": "1.0.0-alpha.23", + "@microsoft/fast-web-utilities": "^6.0.0", + "tabbable": "^6.2.0", + "tslib": "^2.1.0" + }, + "engines": { + "node": "^22.0.0 || ^24.0.0" + }, + "peerDependencies": { + "@microsoft/fast-element": "^2.0.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", + "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@microsoft/fast-element": { + "version": "2.10.3", + "resolved": "http://localhost:4873/@microsoft/fast-element/-/fast-element-2.10.3.tgz", + "integrity": "sha512-IvcXLHJzujJu2Xw4v+zNvBj03GHUY997rEOSDLEym5i5B1gzxMUmMhP3Q8rPbD3YVWCxJCv8+3XPlNduX+mvgw==", + "license": "MIT", + "peer": true + }, + "node_modules/@microsoft/fast-web-utilities": { + "version": "6.0.0", + "resolved": "http://localhost:4873/@microsoft/fast-web-utilities/-/fast-web-utilities-6.0.0.tgz", + "integrity": "sha512-ckCA4Xn91ja1Qz+jhGGL1Q3ZeuRpA5VvYcRA7GzA1NP545sl14bwz3tbHCq8jIk+PL7mkSaIveGMYuJB2L4Izg==", + "license": "MIT", + "dependencies": { + "exenv-es6": "^1.1.1" + } + }, + "node_modules/@swc/helpers": { + "version": "0.5.21", + "resolved": "http://localhost:4873/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.8.0" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "http://localhost:4873/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "http://localhost:4873/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "http://localhost:4873/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/sortablejs": { + "version": "1.15.9", + "resolved": "http://localhost:4873/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", + "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/type-utils": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.57.1", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "http://localhost:4873/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/parser/-/parser-8.57.1.tgz", + "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", + "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.57.1", + "@typescript-eslint/types": "^8.57.1", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", + "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", + "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", + "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1", + "@typescript-eslint/utils": "8.57.1", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/types/-/types-8.57.1.tgz", + "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", + "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.57.1", + "@typescript-eslint/tsconfig-utils": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/visitor-keys": "8.57.1", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.4.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/utils/-/utils-8.57.1.tgz", + "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.1", + "@typescript-eslint/types": "8.57.1", + "@typescript-eslint/typescript-estree": "8.57.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.57.1", + "resolved": "http://localhost:4873/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", + "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.57.1", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/convert-hex": { + "version": "0.1.0", + "resolved": "http://localhost:4873/convert-hex/-/convert-hex-0.1.0.tgz", + "integrity": "sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A==", + "dev": true + }, + "node_modules/convert-string": { + "version": "0.1.0", + "resolved": "http://localhost:4873/convert-string/-/convert-string-0.1.0.tgz", + "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==", + "dev": true + }, + "node_modules/core-js-pure": { + "version": "3.49.0", + "resolved": "http://localhost:4873/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "http://localhost:4873/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "http://localhost:4873/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "http://localhost:4873/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "http://localhost:4873/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.4", + "@esbuild/android-arm": "0.27.4", + "@esbuild/android-arm64": "0.27.4", + "@esbuild/android-x64": "0.27.4", + "@esbuild/darwin-arm64": "0.27.4", + "@esbuild/darwin-x64": "0.27.4", + "@esbuild/freebsd-arm64": "0.27.4", + "@esbuild/freebsd-x64": "0.27.4", + "@esbuild/linux-arm": "0.27.4", + "@esbuild/linux-arm64": "0.27.4", + "@esbuild/linux-ia32": "0.27.4", + "@esbuild/linux-loong64": "0.27.4", + "@esbuild/linux-mips64el": "0.27.4", + "@esbuild/linux-ppc64": "0.27.4", + "@esbuild/linux-riscv64": "0.27.4", + "@esbuild/linux-s390x": "0.27.4", + "@esbuild/linux-x64": "0.27.4", + "@esbuild/netbsd-arm64": "0.27.4", + "@esbuild/netbsd-x64": "0.27.4", + "@esbuild/openbsd-arm64": "0.27.4", + "@esbuild/openbsd-x64": "0.27.4", + "@esbuild/openharmony-arm64": "0.27.4", + "@esbuild/sunos-x64": "0.27.4", + "@esbuild/win32-arm64": "0.27.4", + "@esbuild/win32-ia32": "0.27.4", + "@esbuild/win32-x64": "0.27.4" + } + }, + "node_modules/esbuild-plugin-inline-css": { + "version": "0.0.1", + "resolved": "http://localhost:4873/esbuild-plugin-inline-css/-/esbuild-plugin-inline-css-0.0.1.tgz", + "integrity": "sha512-CF2NPh3RbSdS6n3KgDmi67MhiBjjYJ+x+xi6Ha3ikMrO4owoS0Oj3+QQpIgwAOfTinLyNLbwhMOlht4QE/iqlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fs-extra": "^10.0.0", + "path": "^0.12.7", + "sha256": "^0.2.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", + "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exenv-es6": { + "version": "1.1.1", + "resolved": "http://localhost:4873/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "http://localhost:4873/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs-extra": { + "version": "10.1.0", + "resolved": "http://localhost:4873/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "http://localhost:4873/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "http://localhost:4873/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imask": { + "version": "7.6.1", + "resolved": "http://localhost:4873/imask/-/imask-7.6.1.tgz", + "integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime-corejs3": "^7.24.4" + }, + "engines": { + "npm": ">=4.0.0" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.3", + "resolved": "http://localhost:4873/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "http://localhost:4873/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "http://localhost:4873/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "http://localhost:4873/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "http://localhost:4873/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/path": { + "version": "0.12.7", + "resolved": "http://localhost:4873/path/-/path-0.12.7.tgz", + "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "process": "^0.11.1", + "util": "^0.10.3" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "http://localhost:4873/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "http://localhost:4873/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "http://localhost:4873/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/rimraf": { + "version": "6.1.3", + "resolved": "http://localhost:4873/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "glob": "^13.0.3", + "package-json-from-dist": "^1.0.1" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "http://localhost:4873/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sha256": { + "version": "0.2.0", + "resolved": "http://localhost:4873/sha256/-/sha256-0.2.0.tgz", + "integrity": "sha512-kTWMJUaez5iiT9CcMv8jSq6kMhw3ST0uRdcIWl3D77s6AsLXNXRp3heeqqfu5+Dyfu4hwpQnMzhqHh8iNQxw0w==", + "dev": true, + "dependencies": { + "convert-hex": "~0.1.0", + "convert-string": "~0.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/tabbable": { + "version": "6.4.0", + "resolved": "http://localhost:4873/tabbable/-/tabbable-6.4.0.tgz", + "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "http://localhost:4873/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "http://localhost:4873/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "http://localhost:4873/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "http://localhost:4873/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "http://localhost:4873/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util": { + "version": "0.10.4", + "resolved": "http://localhost:4873/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/Charts.Scripts/package.json b/src/Charts.Scripts/package.json new file mode 100644 index 0000000000..d94c69ead2 --- /dev/null +++ b/src/Charts.Scripts/package.json @@ -0,0 +1,34 @@ +{ + "private": true, + "name": "microsoft.fluentui.aspnetcore.components.charts.assets", + "source": "src/index.ts", + "main": "dist/Microsoft.FluentUI.AspNetCore.Components.Charts.lib.module.js", + "cssFiles": "../Charts/**/*.css", + "cssBundle": "dist/Microsoft.FluentUI.AspNetCore.Components.Charts.bundle.scp.css", + "scripts": { + "build": "node ./esbuild.config.mjs", + "clean": "rimraf ./dist" + }, + "keywords": [], + "author": "", + "license": "ISC", + "type": "module", + "devDependencies": { + "@types/sortablejs": "^1.15.9", + "@typescript-eslint/eslint-plugin": "8.57.1", + "@typescript-eslint/parser": "8.57.1", + "esbuild": "0.27.4", + "esbuild-plugin-inline-css": "0.0.1", + "eslint": "10.2.0", + "glob": "^13.0.6", + "imask": "^7.6.1", + "rimraf": "6.1.3", + "tabbable": "6.4.0", + "typescript": "5.9.3" + }, + "dependencies": { + "@fluentui/web-components": "^3.0.0-rc.13", + "@fluentui/chart-web-components": "0.0.72" + + } +} diff --git a/src/Charts.Scripts/src/BuildConstants.ts b/src/Charts.Scripts/src/BuildConstants.ts new file mode 100644 index 0000000000..d6b245f501 --- /dev/null +++ b/src/Charts.Scripts/src/BuildConstants.ts @@ -0,0 +1,2 @@ +// This file is generated by a tool. Do not change! +export const BUILD_MODE = 'Debug'; diff --git a/src/Charts.Scripts/src/FluentUIChartComponents.ts b/src/Charts.Scripts/src/FluentUIChartComponents.ts new file mode 100644 index 0000000000..6b2bb42fad --- /dev/null +++ b/src/Charts.Scripts/src/FluentUIChartComponents.ts @@ -0,0 +1,25 @@ +import * as FluentUIWebComponents from '@fluentui/web-components'; +import * as FluentUIComponents from '@fluentui/chart-web-components'; +import { defineOnce } from '@core/RegistrationState'; + + + +export namespace Microsoft.FluentUI.Blazor.FluentUIChartComponents { + + export function defineComponents() { + const registry = FluentUIWebComponents.FluentDesignSystem.registry; + + // Register Chart Web Components + defineOnce('fluentui:chart-components:donut-chart', () => { + FluentUIComponents.DonutChartDefinition.define(registry); + }); + + defineOnce('fluentui:chart-components:horizontal-bar-chart', () => { + FluentUIComponents.HorizontalBarChartDefinition.define(registry); + }); + + defineOnce('fluentui:chart-components:horizontal-bar-chart-with-axis', () => { + FluentUIComponents.HorizontalBarChartWithAxisDefinition.define(registry); + }); + } +} diff --git a/src/Charts.Scripts/src/index.ts b/src/Charts.Scripts/src/index.ts new file mode 100644 index 0000000000..7b53baf960 --- /dev/null +++ b/src/Charts.Scripts/src/index.ts @@ -0,0 +1,9 @@ +import { Microsoft as FluentUIChartComponentsFile } from './FluentUIChartComponents'; + + +// Alias +import FluentUIComponents = FluentUIChartComponentsFile.FluentUI.Blazor.FluentUIChartComponents; + + +FluentUIComponents.defineComponents(); + diff --git a/src/Charts.Scripts/tsconfig.json b/src/Charts.Scripts/tsconfig.json new file mode 100644 index 0000000000..b28a85036c --- /dev/null +++ b/src/Charts.Scripts/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Language and Environment */ + "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */ + + /* Modules */ + "module": "ESNext", + "moduleResolution": "bundler", + "lib": [ "ESNext", "DOM" ], + /* Emit */ + "sourceMap": true, /* Create source map files for emitted JavaScript files. */ + + /* Interop Constraints */ + "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */ + "allowSyntheticDefaultImports": true, + // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ + "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */ + + /* Type Checking */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, + + /* Completeness */ + // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ + "skipLibCheck": true, /* Skip type checking all .d.ts files. */ + "baseUrl": ".", + "paths": { + "@core/*": [ "../Core.Scripts/src/*" ] + } + } +} diff --git a/src/Core/Components/Charts/ChartJson.cs b/src/Charts/ChartJson.cs similarity index 100% rename from src/Core/Components/Charts/ChartJson.cs rename to src/Charts/ChartJson.cs diff --git a/src/Core/Components/Charts/FluentDonutChart.razor b/src/Charts/Charts/DonutChart/FluentDonutChart.razor similarity index 100% rename from src/Core/Components/Charts/FluentDonutChart.razor rename to src/Charts/Charts/DonutChart/FluentDonutChart.razor diff --git a/src/Core/Components/Charts/FluentDonutChart.razor.cs b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs similarity index 97% rename from src/Core/Components/Charts/FluentDonutChart.razor.cs rename to src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs index e192a92a62..fd4323ed8e 100644 --- a/src/Core/Components/Charts/FluentDonutChart.razor.cs +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs @@ -3,6 +3,7 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Charts.Models; using Microsoft.FluentUI.AspNetCore.Components.Utilities; namespace Microsoft.FluentUI.AspNetCore.Components.Charts; diff --git a/src/Core/Components/Charts/FluentHorizontalBarChart.razor b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor similarity index 100% rename from src/Core/Components/Charts/FluentHorizontalBarChart.razor rename to src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor diff --git a/src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs similarity index 100% rename from src/Core/Components/Charts/FluentHorizontalBarChart.razor.cs rename to src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs diff --git a/src/Core/Enums/HorizontalBarChartVariant.cs b/src/Charts/Enums/HorizontalBarChartVariant.cs similarity index 100% rename from src/Core/Enums/HorizontalBarChartVariant.cs rename to src/Charts/Enums/HorizontalBarChartVariant.cs diff --git a/src/Charts/GlobalSuppressions.cs b/src/Charts/GlobalSuppressions.cs new file mode 100644 index 0000000000..93a4cafb73 --- /dev/null +++ b/src/Charts/GlobalSuppressions.cs @@ -0,0 +1,8 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Diagnostics.CodeAnalysis; + +[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Expose public types with root namespace", Scope = "namespace", Target = "~N:Microsoft.FluentUI.AspNetCore.Components.Charts")] +[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Enums kept in sync with core library", Scope = "namespace", Target = "~N:Microsoft.FluentUI.AspNetCore.Components.Enums")] diff --git a/src/Charts/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj b/src/Charts/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj new file mode 100644 index 0000000000..1cd7d47189 --- /dev/null +++ b/src/Charts/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj @@ -0,0 +1,173 @@ + + + $(TargetNetVersions) + Microsoft.FluentUI.AspNetCore.Components.Charts + + A set of Blazor components wrapping an extended version of the Fluent UI Chart Web Components. + Fluent UI, Blazor, Charts, Web Components + Microsoft Fluent UI Chart Components for Blazor + A set of Blazor components wrapping an extended version of the Fluent UI Chart Web Components. + README.md + icon.png + + False + enable + enable + latest + + true + true + + true + embedded + + $(SolutionDir)artifacts + + + true + $(SolutionDir).generated + + + true + false + + + + true + link + true + + + + + False + 6 + true + + + + + True + true + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + true + + + + + + + + + + + + + + + + + <_EsprojAssetsToFix Include="@(StaticWebAsset)" Condition="'%(StaticWebAsset.OriginalItemSpec)' != '' AND $([System.String]::Copy('%(StaticWebAsset.OriginalItemSpec)').EndsWith('.esproj'))" /> + + + + + %(Identity) + + + + <_EsprojAssetsToFix Remove="@(_EsprojAssetsToFix)" /> + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + true + true + true + CS1591 + + + + + + + diff --git a/src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs b/src/Charts/Models/DonutChartDataJsonSerializerContext.cs similarity index 100% rename from src/Core/Components/Charts/Models/DonutChartDataJsonSerializerContext.cs rename to src/Charts/Models/DonutChartDataJsonSerializerContext.cs diff --git a/src/Core/Components/Charts/Models/DonutChartModels.cs b/src/Charts/Models/DonutChartModels.cs similarity index 100% rename from src/Core/Components/Charts/Models/DonutChartModels.cs rename to src/Charts/Models/DonutChartModels.cs diff --git a/src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs b/src/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs similarity index 100% rename from src/Core/Components/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs rename to src/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs diff --git a/src/Core/Components/Charts/Models/HorizontalBarChartModels.cs b/src/Charts/Models/HorizontalBarChartModels.cs similarity index 100% rename from src/Core/Components/Charts/Models/HorizontalBarChartModels.cs rename to src/Charts/Models/HorizontalBarChartModels.cs diff --git a/src/Charts/README.md b/src/Charts/README.md new file mode 100644 index 0000000000..c82e98439d --- /dev/null +++ b/src/Charts/README.md @@ -0,0 +1 @@ +# Welcome to the Microsoft Fluent UI Chart Components for Blazor diff --git a/src/Charts/_Imports.razor b/src/Charts/_Imports.razor new file mode 100644 index 0000000000..f9b6d6d8fb --- /dev/null +++ b/src/Charts/_Imports.razor @@ -0,0 +1,9 @@ +@using System +@using System.Diagnostics.CodeAnalysis +@using Microsoft.AspNetCore.Components +@using Microsoft.AspNetCore.Components.Routing +@using Microsoft.AspNetCore.Components.Web +@using Microsoft.AspNetCore.Components.Web.Virtualization +@using Microsoft.FluentUI.AspNetCore.Components +@using Microsoft.FluentUI.AspNetCore.Components.Utilities +@using Microsoft.JSInterop diff --git a/src/Core.Scripts/.npmrc b/src/Core.Scripts/.npmrc index db9d16ace4..642288e0b9 100644 --- a/src/Core.Scripts/.npmrc +++ b/src/Core.Scripts/.npmrc @@ -1,2 +1,3 @@ -registry=https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ +registry=http://localhost:4873/ always-auth=true + diff --git a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj index fe27f6ca51..e8b86aab1a 100644 --- a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj +++ b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json index cb11758011..741dbbbf59 100644 --- a/src/Core.Scripts/package-lock.json +++ b/src/Core.Scripts/package-lock.json @@ -7,7 +7,6 @@ "name": "microsoft.fluentui.aspnetcore.components.assets", "license": "ISC", "dependencies": { - "@fluentui/chart-web-components": "0.0.71", "@fluentui/web-components": "^3.0.0-rc.13" }, "devDependencies": { @@ -16,7 +15,7 @@ "@typescript-eslint/parser": "8.57.1", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.0.3", + "eslint": "10.2.0", "glob": "^13.0.6", "imask": "^7.6.1", "rimraf": "6.1.3", @@ -509,13 +508,13 @@ } }, "node_modules/@eslint/config-array": { - "version": "0.23.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-array/-/config-array-0.23.3.tgz", - "integrity": "sha1-P0qT3VRhacCRMMvRDyQVsTogohk=", + "version": "0.23.5", + "resolved": "http://localhost:4873/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/object-schema": "^3.0.3", + "@eslint/object-schema": "^3.0.5", "debug": "^4.3.1", "minimatch": "^10.2.4" }, @@ -524,22 +523,22 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-helpers/-/config-helpers-0.5.3.tgz", - "integrity": "sha1-ch/mu7kNdLDIDW/yQo5bvLACvss=", + "version": "0.5.5", + "resolved": "http://localhost:4873/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1" + "@eslint/core": "^1.2.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, "node_modules/@eslint/core": { - "version": "1.1.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/core/-/core-1.1.1.tgz", - "integrity": "sha1-RQ89K+LUY8zVERlUQJIla06I3zI=", + "version": "1.2.1", + "resolved": "http://localhost:4873/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -550,9 +549,9 @@ } }, "node_modules/@eslint/object-schema": { - "version": "3.0.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/object-schema/-/object-schema-3.0.3.tgz", - "integrity": "sha1-W/Zx5S44LkrcR6mQbyaZN0Y322s=", + "version": "3.0.5", + "resolved": "http://localhost:4873/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", "dev": true, "license": "Apache-2.0", "engines": { @@ -560,39 +559,19 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.6.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.6.1.tgz", - "integrity": "sha1-655mibVs6LwYVbszCQ5j8/wRXo4=", + "version": "0.7.1", + "resolved": "http://localhost:4873/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@eslint/core": "^1.1.1", + "@eslint/core": "^1.2.1", "levn": "^0.4.1" }, "engines": { "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@fluentui/chart-web-components": { - "version": "0.0.71", - "resolved": "https://registry.npmjs.org/@fluentui/chart-web-components/-/chart-web-components-0.0.71.tgz", - "integrity": "sha512-XJ6XcpOyhnpWgUpSNIh32EHV1sGEF98TAdWJ4x7ZoQpmugYY1yGMRsYCAVCuxMl4r0apQstcySdbJnrtdDY2Kw==", - "license": "MIT", - "dependencies": { - "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "^3.0.0-rc.13", - "@microsoft/fast-web-utilities": "^6.0.0", - "@types/d3-selection": "^3.0.0", - "@types/d3-shape": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-shape": "^3.0.0", - "tabbable": "^6.2.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" - } - }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", @@ -697,27 +676,6 @@ "tslib": "^2.8.0" } }, - "node_modules/@types/d3-path": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", - "license": "MIT" - }, - "node_modules/@types/d3-selection": { - "version": "3.0.11", - "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", - "license": "MIT" - }, - "node_modules/@types/d3-shape": { - "version": "3.1.8", - "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", - "license": "MIT", - "dependencies": { - "@types/d3-path": "*" - } - }, "node_modules/@types/esrecurse": { "version": "4.3.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/esrecurse/-/esrecurse-4.3.1.tgz", @@ -734,8 +692,8 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "resolved": "http://localhost:4873/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true, "license": "MIT" }, @@ -1081,36 +1039,6 @@ "node": ">= 8" } }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/debug/-/debug-4.4.3.tgz", @@ -1204,18 +1132,18 @@ } }, "node_modules/eslint": { - "version": "10.0.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint/-/eslint-10.0.3.tgz", - "integrity": "sha1-Ngp95/JwbrijLKoXypg/AInv5pQ=", + "version": "10.2.0", + "resolved": "http://localhost:4873/eslint/-/eslint-10.2.0.tgz", + "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.3", - "@eslint/config-helpers": "^0.5.2", - "@eslint/core": "^1.1.1", - "@eslint/plugin-kit": "^0.6.1", + "@eslint/config-array": "^0.23.4", + "@eslint/config-helpers": "^0.5.4", + "@eslint/core": "^1.2.0", + "@eslint/plugin-kit": "^0.7.0", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -1226,7 +1154,7 @@ "escape-string-regexp": "^4.0.0", "eslint-scope": "^9.1.2", "eslint-visitor-keys": "^5.0.1", - "espree": "^11.1.1", + "espree": "^11.2.0", "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", diff --git a/src/Core.Scripts/package.json b/src/Core.Scripts/package.json index 0e1818896a..44b1d9022f 100644 --- a/src/Core.Scripts/package.json +++ b/src/Core.Scripts/package.json @@ -19,7 +19,7 @@ "@typescript-eslint/parser": "8.57.1", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.0.3", + "eslint": "10.2.0", "glob": "^13.0.6", "imask": "^7.6.1", "rimraf": "6.1.3", @@ -27,8 +27,6 @@ "typescript": "5.9.3" }, "dependencies": { - "@fluentui/web-components": "^3.0.0-rc.13", - "@fluentui/chart-web-components": "0.0.71" - - } + "@fluentui/web-components": "^3.0.0-rc.13" + } } diff --git a/src/Core.Scripts/src/FluentUIWebComponents.ts b/src/Core.Scripts/src/FluentUIWebComponents.ts index 4b32a31e41..afee409566 100644 --- a/src/Core.Scripts/src/FluentUIWebComponents.ts +++ b/src/Core.Scripts/src/FluentUIWebComponents.ts @@ -1,5 +1,6 @@ import * as FluentUIComponents from '@fluentui/web-components' -import * as FluentUICharts from '@fluentui/chart-web-components' +import { defineOnce } from './RegistrationState'; + export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { @@ -11,51 +12,131 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { // To generate these definitions, run the `_ExtractWebComponents.ps1` file // and paste the output here. - FluentUIComponents.accordionDefinition.define(registry); - FluentUIComponents.accordionItemDefinition.define(registry); - FluentUIComponents.AnchorButtonDefinition.define(registry); - FluentUIComponents.AvatarDefinition.define(registry); - FluentUIComponents.BadgeDefinition.define(registry); - FluentUIComponents.ButtonDefinition.define(registry); - FluentUIComponents.CheckboxDefinition.define(registry); - FluentUIComponents.CompoundButtonDefinition.define(registry); - FluentUIComponents.CounterBadgeDefinition.define(registry); - FluentUIComponents.DialogBodyDefinition.define(registry); - FluentUIComponents.DialogDefinition.define(registry); - FluentUIComponents.DividerDefinition.define(registry); - FluentUIComponents.DrawerBodyDefinition.define(registry); - FluentUIComponents.DrawerDefinition.define(registry); - FluentUIComponents.DropdownDefinition.define(registry); - FluentUIComponents.DropdownOptionDefinition.define(registry); - FluentUIComponents.FieldDefinition.define(registry); - FluentUIComponents.ImageDefinition.define(registry); - FluentUIComponents.LabelDefinition.define(registry); - FluentUIComponents.LinkDefinition.define(registry); - FluentUIComponents.ListboxDefinition.define(registry); - FluentUIComponents.MenuButtonDefinition.define(registry); - FluentUIComponents.MenuDefinition.define(registry); - FluentUIComponents.MenuItemDefinition.define(registry); - FluentUIComponents.MenuListDefinition.define(registry); - FluentUIComponents.MessageBarDefinition.define(registry); - FluentUIComponents.ProgressBarDefinition.define(registry); - FluentUIComponents.RadioDefinition.define(registry); - FluentUIComponents.RadioGroupDefinition.define(registry); - FluentUIComponents.RatingDisplayDefinition.define(registry); - FluentUIComponents.SliderDefinition.define(registry); - FluentUIComponents.SpinnerDefinition.define(registry); - FluentUIComponents.SwitchDefinition.define(registry); - FluentUIComponents.TabDefinition.define(registry); - FluentUIComponents.TablistDefinition.define(registry); - FluentUIComponents.TextAreaDefinition.define(registry); - FluentUIComponents.TextDefinition.define(registry); - FluentUIComponents.TextInputDefinition.define(registry); - FluentUIComponents.ToggleButtonDefinition.define(registry); - FluentUIComponents.TooltipDefinition.define(registry); - FluentUIComponents.TreeDefinition.define(registry); - FluentUIComponents.TreeItemDefinition.define(registry); - - // Register Chart Web Components - FluentUICharts.DonutChartDefinition.define(registry); - FluentUICharts.HorizontalBarChartDefinition.define(registry); + defineOnce('fluentui:web-components:accordion', () => { + FluentUIComponents.accordionDefinition.define(registry); + }); + defineOnce('fluentui:web-components:accordion-item', () => { + FluentUIComponents.accordionItemDefinition.define(registry); + }); + defineOnce('fluentui:web-components:anchor-button', () => { + FluentUIComponents.AnchorButtonDefinition.define(registry); + }); + defineOnce('fluentui:web-components:avatar', () => { + FluentUIComponents.AvatarDefinition.define(registry); + }); + defineOnce('fluentui:web-components:badge', () => { + FluentUIComponents.BadgeDefinition.define(registry); + }); + defineOnce('fluentui:web-components:button', () => { + FluentUIComponents.ButtonDefinition.define(registry); + }); + defineOnce('fluentui:web-components:checkbox', () => { + FluentUIComponents.CheckboxDefinition.define(registry); + }); + defineOnce('fluentui:web-components:compound-button', () => { + FluentUIComponents.CompoundButtonDefinition.define(registry); + }); + defineOnce('fluentui:web-components:counter-badge', () => { + FluentUIComponents.CounterBadgeDefinition.define(registry); + }); + defineOnce('fluentui:web-components:dialog-body', () => { + FluentUIComponents.DialogBodyDefinition.define(registry); + }); + defineOnce('fluentui:web-components:dialog', () => { + FluentUIComponents.DialogDefinition.define(registry); + }); + defineOnce('fluentui:web-components:divider', () => { + FluentUIComponents.DividerDefinition.define(registry); + }); + defineOnce('fluentui:web-components:drawer-body', () => { + FluentUIComponents.DrawerBodyDefinition.define(registry); + }); + defineOnce('fluentui:web-components:drawer', () => { + FluentUIComponents.DrawerDefinition.define(registry); + }); + defineOnce('fluentui:web-components:dropdown', () => { + FluentUIComponents.DropdownDefinition.define(registry); + }); + defineOnce('fluentui:web-components:dropdown-option', () => { + FluentUIComponents.DropdownOptionDefinition.define(registry); + }); + defineOnce('fluentui:web-components:field', () => { + FluentUIComponents.FieldDefinition.define(registry); + }); + defineOnce('fluentui:web-components:image', () => { + FluentUIComponents.ImageDefinition.define(registry); + }); + defineOnce('fluentui:web-components:label', () => { + FluentUIComponents.LabelDefinition.define(registry); + }); + defineOnce('fluentui:web-components:link', () => { + FluentUIComponents.LinkDefinition.define(registry); + }); + defineOnce('fluentui:web-components:listbox', () => { + FluentUIComponents.ListboxDefinition.define(registry); + }); + defineOnce('fluentui:web-components:menu-button', () => { + FluentUIComponents.MenuButtonDefinition.define(registry); + }); + defineOnce('fluentui:web-components:menu', () => { + FluentUIComponents.MenuDefinition.define(registry); + }); + defineOnce('fluentui:web-components:menu-item', () => { + FluentUIComponents.MenuItemDefinition.define(registry); + }); + defineOnce('fluentui:web-components:menu-list', () => { + FluentUIComponents.MenuListDefinition.define(registry); + }); + defineOnce('fluentui:web-components:message-bar', () => { + FluentUIComponents.MessageBarDefinition.define(registry); + }); + defineOnce('fluentui:web-components:progress-bar', () => { + FluentUIComponents.ProgressBarDefinition.define(registry); + }); + defineOnce('fluentui:web-components:radio', () => { + FluentUIComponents.RadioDefinition.define(registry); + }); + defineOnce('fluentui:web-components:radio-group', () => { + FluentUIComponents.RadioGroupDefinition.define(registry); + }); + defineOnce('fluentui:web-components:rating-display', () => { + FluentUIComponents.RatingDisplayDefinition.define(registry); + }); + defineOnce('fluentui:web-components:slider', () => { + FluentUIComponents.SliderDefinition.define(registry); + }); + defineOnce('fluentui:web-components:spinner', () => { + FluentUIComponents.SpinnerDefinition.define(registry); + }); + defineOnce('fluentui:web-components:switch', () => { + FluentUIComponents.SwitchDefinition.define(registry); + }); + defineOnce('fluentui:web-components:tab', () => { + FluentUIComponents.TabDefinition.define(registry); + }); + defineOnce('fluentui:web-components:tablist', () => { + FluentUIComponents.TablistDefinition.define(registry); + }); + defineOnce('fluentui:web-components:text-area', () => { + FluentUIComponents.TextAreaDefinition.define(registry); + }); + defineOnce('fluentui:web-components:text', () => { + FluentUIComponents.TextDefinition.define(registry); + }); + defineOnce('fluentui:web-components:text-input', () => { + FluentUIComponents.TextInputDefinition.define(registry); + }); + defineOnce('fluentui:web-components:toggle-button', () => { + FluentUIComponents.ToggleButtonDefinition.define(registry); + }); + defineOnce('fluentui:web-components:tooltip', () => { + FluentUIComponents.TooltipDefinition.define(registry); + }); + defineOnce('fluentui:web-components:tree', () => { + FluentUIComponents.TreeDefinition.define(registry); + }); + defineOnce('fluentui:web-components:tree-item', () => { + FluentUIComponents.TreeItemDefinition.define(registry); + }); } } diff --git a/src/Core.Scripts/src/RegistrationState.ts b/src/Core.Scripts/src/RegistrationState.ts new file mode 100644 index 0000000000..31f855a905 --- /dev/null +++ b/src/Core.Scripts/src/RegistrationState.ts @@ -0,0 +1,28 @@ +export type RegistrationState = { + definedKeys: Set; +}; + +export function getRegistrationState(): RegistrationState { + const scope = globalThis as typeof globalThis & { + __fluentUIBlazorDefinedKeys__?: RegistrationState; + }; + + if (!scope.__fluentUIBlazorDefinedKeys__) { + scope.__fluentUIBlazorDefinedKeys__ = { + definedKeys: new Set() + }; + } + + return scope.__fluentUIBlazorDefinedKeys__; +} + +export function defineOnce(key: string, callback: () => void) { + const state = getRegistrationState(); + + if (state.definedKeys.has(key)) { + return; + } + + callback(); + state.definedKeys.add(key); +} diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj index af72772611..08dbd6416d 100644 --- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj +++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj @@ -104,7 +104,7 @@ - + + --> + --> + --> + --> + --> From 5ae31d52f1a1038fffd6a7c9330b0705bef0e1b6 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 16 Apr 2026 14:17:05 +0200 Subject: [PATCH 05/60] Add project reference to esproj, remove wrong using statement --- ...ft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj | 7 +++++-- src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj index d4efe8680a..d130563623 100644 --- a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj +++ b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components.Charts @@ -10,4 +10,7 @@ - \ No newline at end of file + + + + diff --git a/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs index fd4323ed8e..e192a92a62 100644 --- a/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs @@ -3,7 +3,6 @@ // ------------------------------------------------------------------------ using Microsoft.AspNetCore.Components; -using Microsoft.FluentUI.AspNetCore.Components.Charts.Models; using Microsoft.FluentUI.AspNetCore.Components.Utilities; namespace Microsoft.FluentUI.AspNetCore.Components.Charts; From 4d1e401ce494f078d0c3ceba026d3cf285cadb84 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 16 Apr 2026 14:21:16 +0200 Subject: [PATCH 06/60] Use .editorconfig instead of surpression file --- .editorconfig | 4 ++++ src/Charts/GlobalSuppressions.cs | 8 -------- src/Core.Scripts/src/FluentUIWebComponents.ts | 2 +- 3 files changed, 5 insertions(+), 9 deletions(-) delete mode 100644 src/Charts/GlobalSuppressions.cs diff --git a/.editorconfig b/.editorconfig index ba5e5c6f96..083520c941 100644 --- a/.editorconfig +++ b/.editorconfig @@ -405,6 +405,10 @@ dotnet_diagnostic.MA0026.severity = none # IDE0130: Namespace does not match folder structure dotnet_diagnostic.IDE0130.severity = none +[src/Charts/**.cs] +# IDE0130: Namespace does not match folder structure +dotnet_diagnostic.IDE0130.severity = none + [{tests/**.{cs,razor},examples/Tools/FluentUI.Demo.DocViewer.Tests/**.{cs,razor}}] # CA1018: Mark attributes with AttributeUsageAttribute dotnet_diagnostic.CA1018.severity = suggestion diff --git a/src/Charts/GlobalSuppressions.cs b/src/Charts/GlobalSuppressions.cs deleted file mode 100644 index 93a4cafb73..0000000000 --- a/src/Charts/GlobalSuppressions.cs +++ /dev/null @@ -1,8 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using System.Diagnostics.CodeAnalysis; - -[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Expose public types with root namespace", Scope = "namespace", Target = "~N:Microsoft.FluentUI.AspNetCore.Components.Charts")] -[assembly: SuppressMessage("Style", "IDE0130:Namespace does not match folder structure", Justification = "Enums kept in sync with core library", Scope = "namespace", Target = "~N:Microsoft.FluentUI.AspNetCore.Components.Enums")] diff --git a/src/Core.Scripts/src/FluentUIWebComponents.ts b/src/Core.Scripts/src/FluentUIWebComponents.ts index afee409566..aa7382aed4 100644 --- a/src/Core.Scripts/src/FluentUIWebComponents.ts +++ b/src/Core.Scripts/src/FluentUIWebComponents.ts @@ -15,7 +15,7 @@ export namespace Microsoft.FluentUI.Blazor.FluentUIWebComponents { defineOnce('fluentui:web-components:accordion', () => { FluentUIComponents.accordionDefinition.define(registry); }); - defineOnce('fluentui:web-components:accordion-item', () => { + defineOnce('fluentui:web-components:accordion-item', () => { FluentUIComponents.accordionItemDefinition.define(registry); }); defineOnce('fluentui:web-components:anchor-button', () => { From e83fba78361d786554b70a8207b5371b23845b65 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Sun, 19 Apr 2026 23:51:02 +0200 Subject: [PATCH 07/60] Internalize the chart code. Devlopement is STILL done in the chart-web-component package but finished source is copied here. This because we cannot publisch/update the npm package --- .../Charts/Examples/DonutChartDefault.razor | 2 +- .../Examples/DonutChartDefaultRTL.razor | 1 + .../Charts/Examples/DonutChartLabels.razor | 24 + .../Examples/HorizontalBarChartDefault.razor | 5 +- .../HorizontalBarChartDefaultRTL.razor | 3 +- .../Charts/Pages/FluentDonutChartPage.md | 4 + .../Components/MarkdownViewer.razor | 1 - .../Components/MarkdownViewer.razor.cs | 8 +- ...spNetCore.Components.Charts.Scripts.esproj | 4 +- src/Charts.Scripts/esbuild.config.mjs | 26 +- src/Charts.Scripts/package-lock.json | 1342 +---------------- src/Charts.Scripts/package.json | 34 +- .../src/FluentUIChartComponents.ts | 2 +- src/Charts.Scripts/src/components.ts | 18 + src/Charts.Scripts/src/donut-chart/define.ts | 4 + .../src/donut-chart/donut-chart.definition.ts | 18 + .../src/donut-chart/donut-chart.options.ts | 34 + .../src/donut-chart/donut-chart.styles.ts | 182 +++ .../src/donut-chart/donut-chart.template.ts | 71 + .../src/donut-chart/donut-chart.ts | 452 ++++++ src/Charts.Scripts/src/donut-chart/index.ts | 4 + .../horizontal-bar-chart-with-axis/define.ts | 4 + .../horizontal-bar-chart-with-axis.bench.ts | 12 + ...rizontal-bar-chart-with-axis.definition.ts | 18 + .../horizontal-bar-chart-with-axis.options.ts | 38 + .../horizontal-bar-chart-with-axis.styles.ts | 178 +++ ...horizontal-bar-chart-with-axis.template.ts | 62 + .../horizontal-bar-chart-with-axis.ts | 1141 ++++++++++++++ .../horizontal-bar-chart-with-axis/index.ts | 4 + .../src/horizontal-bar-chart/define.ts | 4 + .../horizontal-bar-chart.bench.ts | 12 + .../horizontal-bar-chart.definition.ts | 18 + .../horizontal-bar-chart.options.ts | 50 + .../horizontal-bar-chart.styles.ts | 185 +++ .../horizontal-bar-chart.template.ts | 67 + .../horizontal-bar-chart.ts | 600 ++++++++ .../src/horizontal-bar-chart/index.ts | 4 + src/Charts.Scripts/src/index.ts | 4 - src/Charts.Scripts/src/utils/chart-helpers.ts | 217 +++ src/Charts.Scripts/tsconfig.json | 1 + .../Charts/DonutChart/FluentDonutChart.razor | 2 + .../DonutChart/FluentDonutChart.razor.cs | 14 + 42 files changed, 3571 insertions(+), 1303 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor create mode 100644 src/Charts.Scripts/src/components.ts create mode 100644 src/Charts.Scripts/src/donut-chart/define.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.definition.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.options.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.template.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.ts create mode 100644 src/Charts.Scripts/src/donut-chart/index.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/define.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.definition.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.options.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/index.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/define.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.bench.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.definition.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.options.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.styles.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.template.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts create mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/index.ts create mode 100644 src/Charts.Scripts/src/utils/chart-helpers.ts diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor index e577192b29..22ae3ce435 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor @@ -1,10 +1,10 @@ @using Microsoft.FluentUI.AspNetCore.Components.Charts - @code { DonutChartData data = new DonutChartData { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor index 29630b4b39..e1dc6e894f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor @@ -2,6 +2,7 @@
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor new file mode 100644 index 0000000000..6ddef9707e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor @@ -0,0 +1,24 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + +@code { + DonutChartData data = new DonutChartData + { + + ChartData = new List + { + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + } + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor index 29f1acc614..986e090c6c 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor @@ -1,7 +1,8 @@ @using Microsoft.FluentUI.AspNetCore.Components.Charts - + @code { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor index 3334910cc5..6fcecd23a2 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor @@ -1,7 +1,8 @@ @using Microsoft.FluentUI.AspNetCore.Components.Charts
-
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md index 9fcbabff6f..e22c320d6f 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -42,6 +42,10 @@ Donut charts are used to show proportion, which expresses a partial value in com {{ DonutChartDefault }} +### Donut Chart with labels + +{{ DonutChartLabels }} + ### RTL Donut Chart {{ DonutChartDefaultRTL }} diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor index 398d5d1944..5473758abf 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor @@ -1,6 +1,5 @@ @using FluentUI.Demo.DocViewer.Components.Markdown @using FluentUI.Demo.DocViewer.Models -@using FluentUI.Demo.DocViewer.Models.Mcp @using FluentUI.Demo.DocViewer.Services @using Microsoft.AspNetCore.Components.Web diff --git a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs index 99be0c8dfb..beadfcd040 100644 --- a/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs +++ b/examples/Tools/FluentUI.Demo.DocViewer/Components/MarkdownViewer.razor.cs @@ -131,7 +131,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) var componentType = ""; // Convert "MyComponent" to ("MyComponent", "MyType") - var match = Regex.Match(name, @"(\w+)(<|<)(.+)(>|>)"); + var match = ComponentsRexEx().Match(name); if (match.Success) { componentName = match.Groups[1].Value; @@ -165,8 +165,7 @@ protected override async Task OnAfterRenderAsync(bool firstRender) } } - - result.InstanceTypes = listOfTypes.ToArray(); + result.InstanceTypes = [.. listOfTypes]; } return result; @@ -217,4 +216,7 @@ private static string GetLanguageClassName(string? file = null) _ => "language-plaintext" }; } + + [GeneratedRegex(@"(\w+)(<|<)(.+)(>|>)")] + private static partial Regex ComponentsRexEx(); } diff --git a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj index d130563623..4a6abcb4c0 100644 --- a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj +++ b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components.Charts @@ -13,4 +13,4 @@ - + \ No newline at end of file diff --git a/src/Charts.Scripts/esbuild.config.mjs b/src/Charts.Scripts/esbuild.config.mjs index 7cf1a31ed8..f87a52f38c 100644 --- a/src/Charts.Scripts/esbuild.config.mjs +++ b/src/Charts.Scripts/esbuild.config.mjs @@ -30,17 +30,17 @@ await esbuild.build({ }); // CSS: Microsoft.FluentUI.AspNetCore.Components.Charts.bundle.scp.css -const allStyleFile = 'all-styles.css'; -const files = await glob(pkg.cssFiles); -const content = files.map(file => `@import "${file.replace(/\\/g, '/')}";`).join('\n'); +//const allStyleFile = 'all-styles.css'; +//const files = await glob(pkg.cssFiles); +//const content = files.map(file => `@import "${file.replace(/\\/g, '/')}";`).join('\n'); -await writeFile(allStyleFile, content); -await esbuild.build({ - entryPoints: [allStyleFile], - loader: { '.css': 'css' }, - outfile: pkg.cssBundle, - bundle: true, - minify: true, - sourcemap: false, -}); -await unlink(allStyleFile); +//await writeFile(allStyleFile, content); +//await esbuild.build({ +// entryPoints: [allStyleFile], +// loader: { '.css': 'css' }, +// outfile: pkg.cssBundle, +// bundle: true, +// minify: true, +// sourcemap: false, +//}); +//await unlink(allStyleFile); diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json index 1077d72c83..4e0a8c64d0 100644 --- a/src/Charts.Scripts/package-lock.json +++ b/src/Charts.Scripts/package-lock.json @@ -8,34 +8,26 @@ "name": "microsoft.fluentui.aspnetcore.components.charts.assets", "license": "ISC", "dependencies": { - "@fluentui/chart-web-components": "0.0.72", - "@fluentui/web-components": "^3.0.0-rc.13" + "@fluentui/tokens": "^1.0.0-alpha.23", + "@fluentui/web-components": "3.0.0-rc.13", + "@microsoft/fast-web-utilities": "^6.0.0", + "@types/d3-format": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", + "d3-format": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "tslib": "^2.1.0" }, "devDependencies": { - "@types/sortablejs": "^1.15.9", - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", + "@microsoft/fast-element": "2.0.0", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.2.0", "glob": "^13.0.6", - "imask": "^7.6.1", - "rimraf": "6.1.3", - "tabbable": "6.4.0", - "typescript": "5.9.3" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.29.2", - "resolved": "http://localhost:4873/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", - "integrity": "sha512-Lc94FOD5+0aXhdb0Tdg3RUtqT6yWbI/BbFWvlaSJ3gAb9Ks+99nHRDKADVqC37er4eCB0fHyWT+y+K3QOvJKbw==", - "dev": true, - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.48.0" + "rimraf": "6.1.3" }, - "engines": { - "node": ">=6.9.0" + "peerDependencies": { + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -480,134 +472,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha512-Y3kKLvC1dvTOT+oGlqNQ1XLqK6D1HU2YXPc52NmAlJZbMMWDzGYXMiPRJ8TYD39muD/OTjlZmNJ4ib7dvSrMBA==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha512-MwcE1P+AZ4C6DWlpin/OmOA54mmIZ/+xZuJiQd4SyB29oAJjN30UW9wkKNptW2ctp4cEsvhlLY/CsQ1uoHDloQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha512-vqTaUEgxzm+YDSdElad6PiRoX4t8VGDjCtt05zn4nU810UIx/uNEV7/lZJ6KwFThKZOzOxzXy48da+No7HZaMw==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@fluentui/chart-web-components": { - "version": "0.0.72", - "resolved": "http://localhost:4873/@fluentui/chart-web-components/-/chart-web-components-0.0.72.tgz", - "integrity": "sha512-W90OP0ckOOAmTvSDI7U2jORsMUdW6KFfVu1alI3Ccr0bDQGoTZFxIkvIpq18zzWLgdpYKtHBwxdfA62k+QgF2A==", - "license": "MIT", - "dependencies": { - "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "3.0.0-rc.13", - "@microsoft/fast-web-utilities": "^6.0.0", - "@types/d3-selection": "^3.0.0", - "@types/d3-shape": "^3.0.0", - "d3-format": "^3.0.0", - "d3-selection": "^3.0.0", - "d3-shape": "^3.0.0", - "tabbable": "^6.2.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" - } - }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", "resolved": "http://localhost:4873/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", @@ -635,64 +499,11 @@ "@microsoft/fast-element": "^2.0.0" } }, - "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.1", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@microsoft/fast-element": { - "version": "2.10.3", - "resolved": "http://localhost:4873/@microsoft/fast-element/-/fast-element-2.10.3.tgz", - "integrity": "sha512-IvcXLHJzujJu2Xw4v+zNvBj03GHUY997rEOSDLEym5i5B1gzxMUmMhP3Q8rPbD3YVWCxJCv8+3XPlNduX+mvgw==", - "license": "MIT", - "peer": true + "version": "2.0.0", + "resolved": "http://localhost:4873/@microsoft/fast-element/-/fast-element-2.0.0.tgz", + "integrity": "sha512-Tzv4dCGTg10NNyqWWGRy3bYG4vacQUapjy5gpdpmFbSgvNF8aOzJJMkG5CfVUDoC9gWxY1ZyGgjXX0p86ZXX5w==", + "license": "MIT" }, "node_modules/@microsoft/fast-web-utilities": { "version": "6.0.0", @@ -712,6 +523,12 @@ "tslib": "^2.8.0" } }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "http://localhost:4873/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, "node_modules/@types/d3-path": { "version": "3.1.1", "resolved": "http://localhost:4873/@types/d3-path/-/d3-path-3.1.1.tgz", @@ -733,442 +550,92 @@ "@types/d3-path": "*" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha512-xJBAbDifo5hpffDBuHl0Y8ywswbiAp/Wi7Y/GtAgSlZyIABppyurxVueOPE8LUQOxdlgi6Zqce7uoEpqNTeiUw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/sortablejs": { - "version": "1.15.9", - "resolved": "http://localhost:4873/@types/sortablejs/-/sortablejs-1.15.9.tgz", - "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.1.tgz", - "integrity": "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/type-utils": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "ignore": "^7.0.5", - "natural-compare": "^1.4.0", - "ts-api-utils": "^2.4.0" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "@typescript-eslint/parser": "^8.57.1", - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" - } - }, - "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { - "version": "7.0.5", - "resolved": "http://localhost:4873/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@typescript-eslint/parser": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/parser/-/parser-8.57.1.tgz", - "integrity": "sha512-k4eNDan0EIMTT/dUKc/g+rsJ6wcHYhNPdY19VoX/EOtaAG8DLtKCykhrUnuHPYvinn5jhAPgD2Qw9hXBwrahsw==", + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "http://localhost:4873/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", "dev": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3" - }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/project-service": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/project-service/-/project-service-8.57.1.tgz", - "integrity": "sha512-vx1F37BRO1OftsYlmG9xay1TqnjNVlqALymwWVuYTdo18XuKxtBpCj1QlzNIEHlvlB27osvXFWptYiEWsVdYsg==", + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "http://localhost:4873/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.57.1", - "@typescript-eslint/types": "^8.57.1", - "debug": "^4.4.3" + "balanced-match": "^4.0.2" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": "18 || 20 || >=22" } }, - "node_modules/@typescript-eslint/scope-manager": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/scope-manager/-/scope-manager-8.57.1.tgz", - "integrity": "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1" - }, - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } + "node_modules/convert-hex": { + "version": "0.1.0", + "resolved": "http://localhost:4873/convert-hex/-/convert-hex-0.1.0.tgz", + "integrity": "sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A==", + "dev": true }, - "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.1.tgz", - "integrity": "sha512-0lgOZB8cl19fHO4eI46YUx2EceQqhgkPSuCGLlGi79L2jwYY1cxeYc1Nae8Aw1xjgW3PKVDLlr3YJ6Bxx8HkWg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" - } + "node_modules/convert-string": { + "version": "0.1.0", + "resolved": "http://localhost:4873/convert-string/-/convert-string-0.1.0.tgz", + "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==", + "dev": true }, - "node_modules/@typescript-eslint/type-utils": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/type-utils/-/type-utils-8.57.1.tgz", - "integrity": "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1", - "@typescript-eslint/utils": "8.57.1", - "debug": "^4.4.3", - "ts-api-utils": "^2.4.0" - }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "http://localhost:4873/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=12" } }, - "node_modules/@typescript-eslint/types": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/types/-/types-8.57.1.tgz", - "integrity": "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ==", - "dev": true, - "license": "MIT", + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "http://localhost:4873/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" + "node": ">=12" } }, - "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.1.tgz", - "integrity": "sha512-ybe2hS9G6pXpqGtPli9Gx9quNV0TWLOmh58ADlmZe9DguLq0tiAKVjirSbtM1szG6+QH6rVXyU6GTLQbWnMY+g==", - "dev": true, - "license": "MIT", - "dependencies": { - "@typescript-eslint/project-service": "8.57.1", - "@typescript-eslint/tsconfig-utils": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/visitor-keys": "8.57.1", - "debug": "^4.4.3", - "minimatch": "^10.2.2", - "semver": "^7.7.3", - "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.4.0" - }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "http://localhost:4873/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "typescript": ">=4.8.4 <6.0.0" + "node": ">=12" } }, - "node_modules/@typescript-eslint/utils": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/utils/-/utils-8.57.1.tgz", - "integrity": "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ==", - "dev": true, - "license": "MIT", + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "http://localhost:4873/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", "dependencies": { - "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.57.1", - "@typescript-eslint/types": "8.57.1", - "@typescript-eslint/typescript-estree": "8.57.1" + "d3-path": "^3.1.0" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - }, - "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", - "typescript": ">=4.8.4 <6.0.0" + "node": ">=12" } }, - "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.57.1", - "resolved": "http://localhost:4873/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.1.tgz", - "integrity": "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A==", + "node_modules/esbuild": { + "version": "0.27.4", + "resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz", + "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", "dev": true, + "hasInstallScript": true, "license": "MIT", - "dependencies": { - "@typescript-eslint/types": "8.57.1", - "eslint-visitor-keys": "^5.0.0" + "bin": { + "esbuild": "bin/esbuild" }, "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/typescript-eslint" - } - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", - "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/balanced-match": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", - "dev": true, - "license": "MIT", - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^4.0.2" - }, - "engines": { - "node": "18 || 20 || >=22" - } - }, - "node_modules/convert-hex": { - "version": "0.1.0", - "resolved": "http://localhost:4873/convert-hex/-/convert-hex-0.1.0.tgz", - "integrity": "sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A==", - "dev": true - }, - "node_modules/convert-string": { - "version": "0.1.0", - "resolved": "http://localhost:4873/convert-string/-/convert-string-0.1.0.tgz", - "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==", - "dev": true - }, - "node_modules/core-js-pure": { - "version": "3.49.0", - "resolved": "http://localhost:4873/core-js-pure/-/core-js-pure-3.49.0.tgz", - "integrity": "sha512-XM4RFka59xATyJv/cS3O3Kml72hQXUeGRuuTmMYFxwzc9/7C8OYTaIR/Ji+Yt8DXzsFLNhat15cE/JP15HrCgw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/d3-format": { - "version": "3.1.2", - "resolved": "http://localhost:4873/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-path": { - "version": "3.1.0", - "resolved": "http://localhost:4873/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-selection": { - "version": "3.0.0", - "resolved": "http://localhost:4873/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "resolved": "http://localhost:4873/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "node": ">=18" }, "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.4", @@ -1211,267 +678,12 @@ "sha256": "^0.2.0" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", - "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.4", - "@eslint/config-helpers": "^0.5.4", - "@eslint/core": "^1.2.0", - "@eslint/plugin-kit": "^0.7.0", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha512-xS90H51cKw0jltxmvmHy2Iai1LIqrfbw57b79w/J7MfvDfkIkFZ+kj6zC3BjtUwh150HsSSdxXZcsuv72miDFQ==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://registry.npmjs.org/espree/-/espree-11.2.0.tgz", - "integrity": "sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/exenv-es6": { "version": "1.1.1", "resolved": "http://localhost:4873/exenv-es6/-/exenv-es6-1.1.1.tgz", "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", "license": "MIT" }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", - "dev": true, - "license": "MIT" - }, - "node_modules/fdir": { - "version": "6.5.0", - "resolved": "http://localhost:4873/fdir/-/fdir-6.5.0.tgz", - "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "picomatch": "^3 || ^4" - }, - "peerDependenciesMeta": { - "picomatch": { - "optional": true - } - } - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", - "dev": true, - "license": "ISC" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "http://localhost:4873/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1505,19 +717,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "http://localhost:4873/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1525,39 +724,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imask": { - "version": "7.6.1", - "resolved": "http://localhost:4873/imask/-/imask-7.6.1.tgz", - "integrity": "sha512-sJlIFM7eathUEMChTh9Mrfw/IgiWgJqBKq2VNbyXvBZ7ev/IlO6/KQTKlV/Fm+viQMLrFLG/zCuudrLIwgK2dg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime-corejs3": "^7.24.4" - }, - "engines": { - "npm": ">=4.0.0" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.3", "resolved": "http://localhost:4873/inherits/-/inherits-2.0.3.tgz", @@ -1565,57 +731,6 @@ "dev": true, "license": "ISC" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true, - "license": "ISC" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", - "dev": true, - "license": "MIT" - }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "http://localhost:4873/jsonfile/-/jsonfile-6.2.0.tgz", @@ -1629,46 +744,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "11.3.5", "resolved": "http://localhost:4873/lru-cache/-/lru-cache-11.3.5.tgz", @@ -1681,7 +756,7 @@ }, "node_modules/minimatch": { "version": "10.2.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "resolved": "http://localhost:4873/minimatch/-/minimatch-10.2.5.tgz", "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", "dev": true, "license": "BlueOak-1.0.0", @@ -1705,70 +780,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "http://localhost:4873/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1787,26 +798,6 @@ "util": "^0.10.3" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "http://localhost:4873/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1824,29 +815,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "http://localhost:4873/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "http://localhost:4873/process/-/process-0.11.10.tgz", @@ -1857,16 +825,6 @@ "node": ">= 0.6.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/rimraf": { "version": "6.1.3", "resolved": "http://localhost:4873/rimraf/-/rimraf-6.1.3.tgz", @@ -1887,19 +845,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/semver": { - "version": "7.7.4", - "resolved": "http://localhost:4873/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/sha256": { "version": "0.2.0", "resolved": "http://localhost:4873/sha256/-/sha256-0.2.0.tgz", @@ -1910,98 +855,18 @@ "convert-string": "~0.1.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/tabbable": { "version": "6.4.0", "resolved": "http://localhost:4873/tabbable/-/tabbable-6.4.0.tgz", "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", "license": "MIT" }, - "node_modules/tinyglobby": { - "version": "0.2.16", - "resolved": "http://localhost:4873/tinyglobby/-/tinyglobby-0.2.16.tgz", - "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", - "dev": true, - "license": "MIT", - "dependencies": { - "fdir": "^6.5.0", - "picomatch": "^4.0.4" - }, - "engines": { - "node": ">=12.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/SuperchupuDev" - } - }, - "node_modules/ts-api-utils": { - "version": "2.5.0", - "resolved": "http://localhost:4873/ts-api-utils/-/ts-api-utils-2.5.0.tgz", - "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=18.12" - }, - "peerDependencies": { - "typescript": ">=4.8.4" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "http://localhost:4873/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/typescript": { - "version": "5.9.3", - "resolved": "http://localhost:4873/typescript/-/typescript-5.9.3.tgz", - "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "http://localhost:4873/universalify/-/universalify-2.0.1.tgz", @@ -2012,16 +877,6 @@ "node": ">= 10.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util": { "version": "0.10.4", "resolved": "http://localhost:4873/util/-/util-0.10.4.tgz", @@ -2031,45 +886,6 @@ "dependencies": { "inherits": "2.0.3" } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/src/Charts.Scripts/package.json b/src/Charts.Scripts/package.json index d94c69ead2..f53b7c24c8 100644 --- a/src/Charts.Scripts/package.json +++ b/src/Charts.Scripts/package.json @@ -7,28 +7,38 @@ "cssBundle": "dist/Microsoft.FluentUI.AspNetCore.Components.Charts.bundle.scp.css", "scripts": { "build": "node ./esbuild.config.mjs", - "clean": "rimraf ./dist" + "type-check": "node ./scripts/type-check", + "compile": "node ./scripts/compile", + "clean": "rimraf ./dist", + "lint": "eslint . --ext .ts", + "lint:fix": "eslint . --ext .ts --fix", + "format": "prettier -w src/**/*.{ts,html} --ignore-path ../../.prettierignore", + "format:check": "yarn format -c" }, "keywords": [], "author": "", "license": "ISC", "type": "module", "devDependencies": { - "@types/sortablejs": "^1.15.9", - "@typescript-eslint/eslint-plugin": "8.57.1", - "@typescript-eslint/parser": "8.57.1", + "@microsoft/fast-element": "2.0.0", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.2.0", "glob": "^13.0.6", - "imask": "^7.6.1", - "rimraf": "6.1.3", - "tabbable": "6.4.0", - "typescript": "5.9.3" + "rimraf": "6.1.3" }, "dependencies": { - "@fluentui/web-components": "^3.0.0-rc.13", - "@fluentui/chart-web-components": "0.0.72" - + "@fluentui/tokens": "^1.0.0-alpha.23", + "@fluentui/web-components": "3.0.0-rc.13", + "@microsoft/fast-web-utilities": "^6.0.0", + "@types/d3-format": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", + "d3-format": "^3.0.0", + "d3-selection": "^3.0.0", + "d3-shape": "^3.0.0", + "tslib": "^2.1.0" + }, + "peerDependencies": { + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" } } diff --git a/src/Charts.Scripts/src/FluentUIChartComponents.ts b/src/Charts.Scripts/src/FluentUIChartComponents.ts index 6b2bb42fad..dca9822a91 100644 --- a/src/Charts.Scripts/src/FluentUIChartComponents.ts +++ b/src/Charts.Scripts/src/FluentUIChartComponents.ts @@ -1,5 +1,5 @@ import * as FluentUIWebComponents from '@fluentui/web-components'; -import * as FluentUIComponents from '@fluentui/chart-web-components'; +import * as FluentUIComponents from './components'; import { defineOnce } from '@core/RegistrationState'; diff --git a/src/Charts.Scripts/src/components.ts b/src/Charts.Scripts/src/components.ts new file mode 100644 index 0000000000..509f5657f9 --- /dev/null +++ b/src/Charts.Scripts/src/components.ts @@ -0,0 +1,18 @@ +export { + DonutChart, + DonutChartDefinition, + DonutChartStyles, + DonutChartTemplate +} from './donut-chart/index.js'; +export { + HorizontalBarChart, + HorizontalBarChartDefinition, + HorizontalBarChartStyles, + HorizontalBarChartTemplate, +} from './horizontal-bar-chart/index.js'; +export { + HorizontalBarChartWithAxis, + HorizontalBarChartWithAxisDefinition, + HorizontalBarChartWithAxisStyles, + HorizontalBarChartWithAxisTemplate, +} from './horizontal-bar-chart-with-axis/index.js'; diff --git a/src/Charts.Scripts/src/donut-chart/define.ts b/src/Charts.Scripts/src/donut-chart/define.ts new file mode 100644 index 0000000000..1b8e20ac99 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { definition } from './donut-chart.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.definition.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.definition.ts new file mode 100644 index 0000000000..3cf608dc04 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.definition.ts @@ -0,0 +1,18 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { DonutChart } from './donut-chart.js'; +import { styles } from './donut-chart.styles.js'; +import { template } from './donut-chart.template.js'; + +/** + * @public + * @remarks + * HTML Element: `` + */ +export const definition = DonutChart.compose({ + name: `${FluentDesignSystem.prefix}-donut-chart`, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts new file mode 100644 index 0000000000..484281fd04 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts @@ -0,0 +1,34 @@ +export interface ChartDataPoint { + /** + * Legend text for the datapoint in the chart + */ + legend: string; + + /** + * data the datapoint in the chart + */ + data: number; + + /** + * Color for the legend in the chart. If not provided, it will fallback on the default color palette. + */ + color?: string; + + /** + * Callout data shown in the tooltip and the center label when the segment is highlighted. + * If not provided, the numeric data value is used. + */ + calloutData?: string; +} + +export interface ChartProps { + /** + * data for the points in the chart + */ + chartData: ChartDataPoint[]; +} + +export type Legend = { + legend: string; + color: string; +}; diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts new file mode 100644 index 0000000000..789ef91ce7 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts @@ -0,0 +1,182 @@ +import { css } from '@microsoft/fast-element'; +import { + borderRadiusMedium, + colorNeutralBackground1, + colorNeutralForeground1, + colorNeutralShadowAmbient, + colorNeutralShadowKey, + colorStrokeFocus1, + colorStrokeFocus2, + colorTransparentStroke, + display, + forcedColorsStylesheetBehavior, + spacingHorizontalL, + spacingHorizontalNone, + spacingHorizontalS, + spacingVerticalL, + spacingVerticalMNudge, + spacingVerticalNone, + spacingVerticalS, + strokeWidthThickest, + strokeWidthThin, + typographyBody1StrongStyles, + typographyBody1Styles, + typographyCaption1Styles, + typographyCaption1StrongStyles, + typographyTitle2Styles, + typographyTitle3Styles, +} from '@fluentui/web-components'; + +/** + * Styles for the DonutChart component. + * + * @public + */ +export const styles = css` + ${display('inline-block')} + + :host { + ${typographyBody1Styles} + align-items: center; + flex-direction: column; + width: 100%; + height: 100%; + position: relative; + } + + .chart { + box-sizing: content-box; + overflow: visible; + display: block; + } + + .chart-title { + ${typographyBody1StrongStyles} + color: ${colorNeutralForeground1}; + margin-bottom: ${spacingVerticalS}; + } + + .arc.inactive { + opacity: 0.1; + } + + .arc:focus { + outline: none; + stroke-width: ${strokeWidthThin}; + stroke: ${colorStrokeFocus1}; + } + + .arc-outline { + fill: none; + } + + .arc-outline:has(+ .arc:focus) { + stroke-width: ${strokeWidthThickest}; + stroke: ${colorStrokeFocus2}; + } + + .text-inside-donut { + ${typographyTitle3Styles} + fill: ${colorNeutralForeground1}; + } + + .arc-label { + ${typographyCaption1StrongStyles} + fill: ${colorNeutralForeground1}; + pointer-events: none; + user-select: none; + } + + .arc-label.inactive { + opacity: 0.1; + } + + .legend-container { + padding-top: ${spacingVerticalL}; + white-space: nowrap; + width: 100%; + align-items: center; + margin: -${spacingVerticalS} ${spacingHorizontalNone} ${spacingVerticalNone} -${spacingHorizontalS}; + flex-wrap: wrap; + display: flex; + } + + .legend { + display: flex; + align-items: center; + cursor: pointer; + border: none; + padding: ${spacingHorizontalS}; + background: none; + text-transform: capitalize; + } + + .legend-rect { + width: 12px; + height: 12px; + margin-inline-end: ${spacingHorizontalS}; + border: ${strokeWidthThin} solid; + } + + .legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + } + + .legend.inactive .legend-rect { + background-color: transparent !important; + } + + .legend.inactive .legend-text { + opacity: 0.67; + } + + .tooltip { + display: grid; + overflow: hidden; + padding: ${spacingVerticalMNudge} ${spacingHorizontalL}; + background-color: ${colorNeutralBackground1}; + background-blend-mode: normal, luminosity; + border-radius: ${borderRadiusMedium}; + border: 1px solid ${colorTransparentStroke}; + filter: drop-shadow(0 0 2px ${colorNeutralShadowAmbient}) drop-shadow(0 8px 16px ${colorNeutralShadowKey}); + position: absolute; + z-index: 1; + pointer-events: none; + } + + .tooltip-body { + padding-inline-start: ${spacingHorizontalS}; + color: ${colorNeutralForeground1}; + border-inline-start: 4px solid; + } + + .tooltip-legend-text { + ${typographyCaption1Styles} + } + + .tooltip-content-y { + ${typographyTitle2Styles} + } +`.withBehaviors( + forcedColorsStylesheetBehavior(css` + .text-inside-donut { + fill: CanvasText; + } + + .arc-label { + fill: CanvasText; + } + + .legend-rect, + .tooltip-body { + forced-color-adjust: none; + } + + .tooltip-legend-text, + .tooltip-content-y { + forced-color-adjust: auto; + color: CanvasText; + } + `), +); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts new file mode 100644 index 0000000000..925c1d06bb --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts @@ -0,0 +1,71 @@ +import { ElementViewTemplate, html, ref, repeat, when } from '@microsoft/fast-element'; +import type { DonutChart } from './donut-chart.js'; +import type { Legend } from './donut-chart.options.js'; + +/** + * Generates a template for the DonutChart component. + * + * @public + */ +export function donutChartTemplate(): ElementViewTemplate { + return html` + + `; +} + +/** + * @internal + */ +export const template: ElementViewTemplate = donutChartTemplate(); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.ts new file mode 100644 index 0000000000..493696178b --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.ts @@ -0,0 +1,452 @@ +import { attr, FASTElement, nullableNumberConverter, observable } from '@microsoft/fast-element'; +import { format as d3Format } from 'd3-format'; +import { arc as d3Arc, pie as d3Pie, PieArcDatum } from 'd3-shape'; +import { + getColorFromToken, + getNextColor, + getRTL, + jsonConverter, + booleanStringConverter, + SVG_NAMESPACE_URI, + validateChartProps, + wrapText, +} from '../utils/chart-helpers.js'; +import type { ChartDataPoint, ChartProps, Legend } from './donut-chart.options.js'; + +export class DonutChart extends FASTElement { + @attr({ attribute: 'chart-title' }) + public chartTitle?: string; + + @attr({ converter: nullableNumberConverter }) + public height: number = 200; + + @attr({ converter: nullableNumberConverter }) + public width: number = 200; + + @attr({ attribute: 'hide-legends', converter: booleanStringConverter }) + public hideLegends: boolean = false; + + @attr({ attribute: 'hide-tooltip', converter: booleanStringConverter }) + public hideTooltip: boolean = false; + + @attr({ attribute: 'hide-labels', converter: booleanStringConverter }) + public hideLabels: boolean = true; + + @attr({ attribute: 'show-labels-in-percent', converter: booleanStringConverter }) + public showLabelsInPercent: boolean = false; + + @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + public roundCorners: boolean = false; + + @attr + public order: 'default' | 'sorted' = 'default'; + + @attr({ converter: jsonConverter }) + public data!: ChartProps; + + @attr({ attribute: 'inner-radius', converter: nullableNumberConverter }) + public innerRadius: number = 1; + + @attr({ attribute: 'value-inside-donut' }) + public valueInsideDonut?: string; + + @attr({ attribute: 'legend-list-label' }) + public legendListLabel?: string; + + @attr + public culture?: string; + + @observable + public legends: Legend[] = []; + + @observable + public activeLegend: string = ''; + protected activeLegendChanged(oldValue: string, newValue: string) { + if (newValue === '') { + this._arcs?.forEach(arc => arc.classList.remove('inactive')); + this._arcLabels?.forEach(label => label.classList.remove('inactive')); + } else { + this._arcs?.forEach(arc => { + if (arc.getAttribute('data-id') === newValue) { + arc.classList.remove('inactive'); + } else { + arc.classList.add('inactive'); + } + }); + this._arcLabels?.forEach(label => { + if (label.getAttribute('data-id') === newValue) { + label.classList.remove('inactive'); + } else { + label.classList.add('inactive'); + } + }); + } + + this._updateTextInsideDonut(); + } + + @observable + public isLegendSelected: boolean = false; + + @observable + public tooltipProps = { + isVisible: false, + legend: '', + yValue: '', + color: '', + xPos: 0, + yPos: 0, + }; + protected tooltipPropsChanged(oldValue: any, newValue: any) { + this._updateTextInsideDonut(); + } + + public chartContainer!: HTMLDivElement; + public group!: SVGGElement; + public elementInternals: ElementInternals = this.attachInternals(); + + private _arcs: SVGPathElement[] = []; + private _arcLabels: SVGTextElement[] = []; + private _isRTL: boolean = false; + private _textInsideDonut?: SVGTextElement; + private readonly _handleMouseLeave = () => { + this.tooltipProps = { isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }; + }; + + constructor() { + super(); + + this.elementInternals.role = 'region'; + } + + public handleLegendMouseoverAndFocus(legendTitle: string) { + if (this.isLegendSelected) { + return; + } + + this.activeLegend = legendTitle; + } + + public handleLegendMouseoutAndBlur() { + if (this.isLegendSelected) { + return; + } + + this.activeLegend = ''; + } + + public handleLegendClick(legendTitle: string) { + if (this.isLegendSelected && this.activeLegend === legendTitle) { + this.activeLegend = ''; + this.isLegendSelected = false; + } else { + this.activeLegend = legendTitle; + this.isLegendSelected = true; + } + } + + connectedCallback() { + this._initializeFromAttributes(); + + super.connectedCallback(); + this.addEventListener('mouseleave', this._handleMouseLeave); + + if (!this.data) { + return; + } + + this._initializeAndRender(); + } + + protected dataChanged(_oldValue: ChartProps, newValue: ChartProps) { + if (this.$fastController.isConnected && newValue) { + this._rerender(); + } + } + + protected orderChanged() { + this._rerender(); + } + + protected chartTitleChanged() { + this._rerender(); + } + + protected heightChanged() { + this._rerender(); + } + + protected widthChanged() { + this._rerender(); + } + + protected hideLabelsChanged() { + this._rerender(); + } + + protected showLabelsInPercentChanged() { + this._rerender(); + } + + protected cultureChanged() { + this._rerender(); + } + + protected roundCornersChanged() { + this._rerender(); + } + + private _rerender() { + if (!this.$fastController.isConnected || !this.data) { + return; + } + this._clearChart(); + this._initializeAndRender(); + } + + private _initializeAndRender() { + validateChartProps(this.data, 'data'); + + const chartData = + this.order === 'sorted' ? [...this.data.chartData].sort((a, b) => b.data - a.data) : this.data.chartData; + + chartData.forEach((dataPoint, index) => { + if (dataPoint.color) { + dataPoint.color = getColorFromToken(dataPoint.color); + } else { + dataPoint.color = getNextColor(index); + } + }); + + this.legends = this._getLegends(chartData); + this._isRTL = getRTL(this); + this.elementInternals.ariaLabel = this.chartTitle || `Donut chart with ${chartData.length} segments.`; + + this._render(chartData); + } + + private _initializeFromAttributes() { + const setString = (name: string, assign: (value: string) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(value); + } + }; + + const setBoolean = (name: string, assign: (value: boolean) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(booleanStringConverter.fromView(value)); + } + }; + + const setNumber = (name: string, assign: (value: number) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + const parsed = nullableNumberConverter.fromView(value); + if (parsed !== null && parsed !== undefined) { + assign(parsed); + } + } + }; + + setString('data', value => { this.data = jsonConverter.fromView(value) as ChartProps; }); + setString('chart-title', value => { this.chartTitle = value; }); + setString('value-inside-donut', value => { this.valueInsideDonut = value; }); + setString('legend-list-label', value => { this.legendListLabel = value; }); + setString('culture', value => { this.culture = value; }); + setString('order', value => { this.order = value as 'default' | 'sorted'; }); + + setNumber('inner-radius', value => { this.innerRadius = value; }); + setNumber('height', value => { this.height = value; }); + setNumber('width', value => { this.width = value; }); + + setBoolean('hide-legends', value => { this.hideLegends = value; }); + setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); + setBoolean('hide-labels', value => { this.hideLabels = value; }); + setBoolean('show-labels-in-percent', value => { this.showLabelsInPercent = value; }); + setBoolean('round-corners', value => { this.roundCorners = value; }); + } + + private _clearChart() { + while (this.group.firstChild) { + this.group.removeChild(this.group.firstChild); + } + this._arcs = []; + this._arcLabels = []; + this._textInsideDonut = undefined; + } + + private _render(chartData: ChartDataPoint[]) { + const totalValue = chartData.reduce((total, dataPoint) => total + dataPoint.data, 0); + const outerRadius = (Math.min(this.height, this.width) - 20) / 2; + const cornerRadius = this.roundCorners ? 3 : 0; + const pie = d3Pie() + .value(d => d.data) + .padAngle(0.02); + const arc = d3Arc>() + .innerRadius(this.innerRadius) + .outerRadius(outerRadius) + .cornerRadius(cornerRadius); + + pie(chartData).forEach(arcDatum => { + const arcGroup = document.createElementNS(SVG_NAMESPACE_URI, 'g'); + this.group.appendChild(arcGroup); + + const pathOutline = document.createElementNS(SVG_NAMESPACE_URI, 'path'); + arcGroup.appendChild(pathOutline); + pathOutline.classList.add('arc-outline'); + pathOutline.setAttribute('d', arc(arcDatum)!); + + const path = document.createElementNS(SVG_NAMESPACE_URI, 'path'); + arcGroup.appendChild(path); + this._arcs.push(path); + path.classList.add('arc'); + path.setAttribute('d', arc(arcDatum)!); + path.setAttribute('fill', arcDatum.data.color!); + path.setAttribute('data-id', arcDatum.data.legend); + path.setAttribute('tabindex', '0'); + path.setAttribute('aria-label', `${arcDatum.data.legend}, ${arcDatum.data.data}.`); + path.setAttribute('role', 'img'); + + path.addEventListener('mouseover', event => { + if (this.activeLegend !== '' && this.activeLegend !== arcDatum.data.legend) { + return; + } + + const bounds = this.getBoundingClientRect(); + + this.tooltipProps = { + isVisible: true, + legend: arcDatum.data.legend, + yValue: `${arcDatum.data.data}`, + color: arcDatum.data.color!, + xPos: this._isRTL ? bounds.right - event.clientX : event.clientX - bounds.left, + yPos: event.clientY - bounds.top - 85, + }; + }); + path.addEventListener('focus', event => { + if (this.activeLegend !== '' && this.activeLegend !== arcDatum.data.legend) { + return; + } + + const rootBounds = this.getBoundingClientRect(); + const arcBounds = path.getBoundingClientRect(); + + this.tooltipProps = { + isVisible: true, + legend: arcDatum.data.legend, + yValue: `${arcDatum.data.data}`, + color: arcDatum.data.color!, + xPos: this._isRTL + ? rootBounds.right - arcBounds.left - arcBounds.width / 2 + : arcBounds.left + arcBounds.width / 2 - rootBounds.left, + yPos: arcBounds.top - rootBounds.top - 85, + }; + }); + path.addEventListener('blur', event => { + this.tooltipProps = { isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }; + }); + + const label = this._createArcLabel(arc, arcDatum, totalValue, outerRadius); + if (label) { + arcGroup.appendChild(label); + this._arcLabels.push(label); + } + }); + + if (this.valueInsideDonut) { + this._textInsideDonut = document.createElementNS(SVG_NAMESPACE_URI, 'text'); + this.group.appendChild(this._textInsideDonut); + this._textInsideDonut.classList.add('text-inside-donut'); + this._textInsideDonut.setAttribute('x', '0'); + this._textInsideDonut.setAttribute('y', '0'); + this._textInsideDonut.setAttribute('text-anchor', 'middle'); + this._textInsideDonut.setAttribute('dominant-baseline', 'middle'); + this._updateTextInsideDonut(); + } + } + + private _getLegends(chartData: ChartDataPoint[]): Legend[] { + return chartData.map((d, index) => ({ + legend: d.legend, + color: d.color!, + })); + } + + private _createArcLabel( + arc: ReturnType>>, + arcDatum: PieArcDatum, + totalValue: number, + outerRadius: number, + ) { + if (this.hideLabels || Math.abs(arcDatum.endAngle - arcDatum.startAngle) < Math.PI / 12) { + return undefined; + } + + const [base, perp] = arc.centroid(arcDatum); + const hypotenuse = Math.sqrt(base * base + perp * perp); + const labelRadius = Math.max(this.innerRadius, outerRadius) + 2; + const angle = (arcDatum.startAngle + arcDatum.endAngle) / 2; + const label = document.createElementNS(SVG_NAMESPACE_URI, 'text'); + + label.classList.add('arc-label'); + label.setAttribute('data-id', arcDatum.data.legend); + label.setAttribute('x', `${(hypotenuse === 0 ? 0 : base / hypotenuse) * labelRadius}`); + label.setAttribute('y', `${(hypotenuse === 0 ? 0 : perp / hypotenuse) * labelRadius}`); + label.setAttribute('text-anchor', angle > Math.PI !== this._isRTL ? 'end' : 'start'); + label.setAttribute('dominant-baseline', angle > Math.PI / 2 && angle < (3 * Math.PI) / 2 ? 'hanging' : 'auto'); + label.setAttribute('aria-hidden', 'true'); + label.textContent = this.showLabelsInPercent + ? d3Format('.0%')(totalValue === 0 ? 0 : arcDatum.value / totalValue) + : this._formatArcLabelValue(arcDatum.value); + + return label; + } + + private _formatArcLabelValue(value: number) { + const formatted = new Intl.NumberFormat(this.culture || undefined, { + maximumFractionDigits: value >= 1000 ? 1 : 2, + notation: value >= 1000 ? 'compact' : 'standard', + }).format(value); + + return formatted.endsWith('K') ? `${formatted.slice(0, -1)}k` : formatted; + } + + private _getTextInsideDonut(valueInsideDonut: string) { + let textInsideDonut = valueInsideDonut; + + if (valueInsideDonut && (this.activeLegend !== '' || this.tooltipProps.isVisible)) { + const highlightedDataPoint = this.data.chartData.find( + dataPoint => + dataPoint.legend === this.activeLegend || + (this.tooltipProps.isVisible && dataPoint.legend === this.tooltipProps.legend), + ); + if (this.showLabelsInPercent) { + const total = this.data.chartData.reduce((acc, point) => acc + point.data, 0); + const percentage = total > 0 ? Math.round(((highlightedDataPoint?.data ?? 0) / total) * 100) : 0; + textInsideDonut = `${percentage}%`; + } else { + textInsideDonut = + highlightedDataPoint!.calloutData ?? highlightedDataPoint!.data.toLocaleString(this.culture || undefined); + } + } + + return textInsideDonut; + } + + private _updateTextInsideDonut() { + if (!this._textInsideDonut || !this.valueInsideDonut) { + return; + } + + this._textInsideDonut.textContent = this._getTextInsideDonut(this.valueInsideDonut); + const lineHeight = this._textInsideDonut.getBoundingClientRect().height; + wrapText(this._textInsideDonut, 2 * this.innerRadius); + const lines = this._textInsideDonut.getElementsByTagName('tspan'); + const start = -1 * Math.trunc((lines.length - 1) / 2); + for (let i = 0; i < lines.length; i++) { + lines[i].setAttribute('dy', `${(start + i) * lineHeight}`); + } + } +} diff --git a/src/Charts.Scripts/src/donut-chart/index.ts b/src/Charts.Scripts/src/donut-chart/index.ts new file mode 100644 index 0000000000..60e54e1a59 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/index.ts @@ -0,0 +1,4 @@ +export { definition as DonutChartDefinition } from './donut-chart.definition.js'; +export { DonutChart } from './donut-chart.js'; +export { styles as DonutChartStyles } from './donut-chart.styles.js'; +export { template as DonutChartTemplate } from './donut-chart.template.js'; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/define.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/define.ts new file mode 100644 index 0000000000..e04c840516 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { definition } from './horizontal-bar-chart-with-axis.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts new file mode 100644 index 0000000000..31f68df997 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts @@ -0,0 +1,12 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { definition } from './horizontal-bar-chart-with-axis.definition.js'; + +definition.define(FluentDesignSystem.registry); + +const itemRenderer = () => { + const chart = document.createElement('fluent-horizontal-bar-chart-with-axis'); + return chart; +}; + +export default itemRenderer; +export { tests } from '../utils/benchmark-wrapper.js'; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.definition.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.definition.ts new file mode 100644 index 0000000000..013acb2e30 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.definition.ts @@ -0,0 +1,18 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { HorizontalBarChartWithAxis } from './horizontal-bar-chart-with-axis.js'; +import { styles } from './horizontal-bar-chart-with-axis.styles.js'; +import { template } from './horizontal-bar-chart-with-axis.template.js'; + +/** + * @public + * @remarks + * HTML Element: `` + */ +export const definition = HorizontalBarChartWithAxis.compose({ + name: `${FluentDesignSystem.prefix}-horizontal-bar-chart-with-axis`, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.options.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.options.ts new file mode 100644 index 0000000000..7838178b45 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.options.ts @@ -0,0 +1,38 @@ +export interface AccessibilityData { + ariaLabel?: string; +} + +export interface HorizontalBarChartWithAxisDataPoint { + x: number; + y: number | string; + legend?: string; + color?: string; + gradient?: [string, string]; + xAxisCalloutData?: string; + yAxisCalloutData?: string; + onClick?: VoidFunction; + callOutAccessibilityData?: AccessibilityData; +} + +export type AxisCategoryOrder = + | 'default' + | 'data' + | 'category ascending' + | 'category descending' + | 'total ascending' + | 'total descending' + | 'min ascending' + | 'min descending' + | 'max ascending' + | 'max descending' + | 'sum ascending' + | 'sum descending' + | 'mean ascending' + | 'mean descending' + | 'median ascending' + | 'median descending'; + +export interface HorizontalBarChartWithAxisLegend { + legend: string; + color: string; +} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts new file mode 100644 index 0000000000..376f23d78b --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts @@ -0,0 +1,178 @@ +import type { ElementStyles } from '@microsoft/fast-element'; +import { css } from '@microsoft/fast-element'; +import { + colorNeutralBackground1, + colorNeutralForeground1, + colorNeutralForeground2, + colorNeutralStroke1, + colorNeutralStrokeAccessible, + display, + fontSizeHero700, + forcedColorsStylesheetBehavior, + shadow4, + spacingHorizontalL, + spacingHorizontalNone, + spacingHorizontalS, + spacingVerticalL, + spacingVerticalMNudge, + spacingVerticalNone, + spacingVerticalS, + spacingVerticalXS, + strokeWidthThick, + strokeWidthThickest, + strokeWidthThin, + typographyBody1StrongStyles, + typographyBody1Styles, + typographyCaption1Styles, + typographySubtitle2StrongerStyles, +} from '@fluentui/web-components'; + +export const styles: ElementStyles = css` + ${display('inline-block')} + + :host { + position: relative; + width: 100%; + } + + .chart-title { + ${typographyBody1StrongStyles} + margin-bottom: ${spacingVerticalS}; + } + + .chart-svg { + display: block; + overflow: visible; + } + + .axis-domain, + .origin-line { + stroke: ${colorNeutralStroke1}; + stroke-width: 1; + opacity: 0.2; + } + + .axis-tick-line { + stroke: ${colorNeutralForeground1}; + stroke-width: 1; + opacity: 0.24; + } + + .axis-text, + .y-axis-text { + ${typographyCaption1Styles} + fill: ${colorNeutralForeground2}; + font-size: 10px; + font-weight: 600; + } + + .bar { + opacity: 1; + } + + .bar.inactive { + opacity: 0.1; + } + + .bar:focus { + outline: none; + stroke-width: ${strokeWidthThick}; + stroke: black; + } + + .bar-label { + ${typographyBody1StrongStyles} + fill: ${colorNeutralForeground1}; + direction: ltr; + unicode-bidi: isolate; + } + + .legend-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding-top: ${spacingVerticalL}; + width: 100%; + align-items: center; + margin: -${spacingVerticalS} ${spacingHorizontalNone} ${spacingVerticalNone} -${spacingHorizontalS}; + } + + .legend { + display: flex; + align-items: center; + cursor: pointer; + border: none; + padding: ${spacingHorizontalS}; + background: none; + } + + .legend-rect { + width: 12px; + height: 12px; + margin-inline-end: ${spacingHorizontalS}; + border: ${strokeWidthThin} solid; + } + + .legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + } + + .legend.inactive .legend-rect { + background-color: transparent !important; + } + + .legend.inactive .legend-text { + opacity: 0.67; + } + + .tooltip { + ${typographyCaption1Styles} + position: absolute; + z-index: 999; + display: grid; + overflow: hidden; + padding: ${spacingVerticalMNudge} ${spacingHorizontalL}; + background: ${colorNeutralBackground1}; + box-shadow: ${shadow4}; + border: ${strokeWidthThick}; + pointer-events: none; + } + + .tooltip-header { + ${typographyCaption1Styles} + color: ${colorNeutralForeground2}; + opacity: 0.8; + } + + .tooltip-info { + margin-top: 11px; + padding-inline-start: ${spacingHorizontalS}; + border-inline-start: ${strokeWidthThickest} solid; + } + + .tooltip-legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + text-align: start; + margin-bottom: ${spacingVerticalXS}; + } + + .tooltip-primary-value { + ${typographySubtitle2StrongerStyles} + font-size: ${fontSizeHero700}; + direction: ltr; + unicode-bidi: isolate; + } +`.withBehaviors( + forcedColorsStylesheetBehavior(css` + .legend-rect, + .tooltip-info { + forced-color-adjust: none; + } + + .bar-label { + fill: CanvasText !important; + } + `), +); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts new file mode 100644 index 0000000000..b14afdacf6 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts @@ -0,0 +1,62 @@ +import { ElementViewTemplate, html, ref, repeat, when } from '@microsoft/fast-element'; +import type { HorizontalBarChartWithAxis } from './horizontal-bar-chart-with-axis.js'; +import type { HorizontalBarChartWithAxisLegend } from './horizontal-bar-chart-with-axis.options.js'; + +export function horizontalBarChartWithAxisTemplate(): ElementViewTemplate { + return html` + + `; +} + +export const template: ElementViewTemplate = horizontalBarChartWithAxisTemplate(); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts new file mode 100644 index 0000000000..8962554af9 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts @@ -0,0 +1,1141 @@ +import { attr, FASTElement, observable } from '@microsoft/fast-element'; +import { + getColorFromToken, + getNextColor, + getRTL, + jsonConverter, + booleanStringConverter, + SVG_NAMESPACE_URI +} from '../utils/chart-helpers.js'; +import type { + AxisCategoryOrder, + HorizontalBarChartWithAxisDataPoint, + HorizontalBarChartWithAxisLegend, +} from './horizontal-bar-chart-with-axis.options.js'; + +type TooltipProps = { + isVisible: boolean; + legend: string; + xLabel: string; + xValue: string; + yLabel: string; + yValue: string; + color: string; + xPos: number; + yPos: number; +}; + +type GroupedSeries = { + key: string; + rawY: number | string; + points: HorizontalBarChartWithAxisDataPoint[]; +}; + +type RenderedBar = { + legend?: string; + element: SVGRectElement; +}; + +type PlotLayout = { + barHeight: number; + margins: { + top: number; + right: number; + bottom: number; + left: number; + }; + innerHeight: number; +}; + +const X_AXIS_LABEL = 'X'; +const Y_AXIS_LABEL = 'Y'; +const DEFAULT_HEIGHT = 320; +const DEFAULT_BAR_HEIGHT = 32; +const DEFAULT_X_TICK_COUNT = 6; +const DEFAULT_Y_TICK_COUNT = 4; +const DEFAULT_Y_AXIS_PADDING = 0.5; + +const MIN_DOMAIN_MARGIN = 8; +const STACKED_SEGMENT_GAP = 2; +const createSvgElement = (tag: string): T => { + return document.createElementNS(SVG_NAMESPACE_URI, tag) as T; +}; + +const toNumber = (value: number | string | undefined, fallback: number): number => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : fallback; +}; + +const toOptionalNumber = (value: number | string | undefined): number | undefined => { + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +}; + +const formatCompactNumber = (value: number, culture?: string) => { + return new Intl.NumberFormat(culture || undefined, { + maximumFractionDigits: Math.abs(value) >= 1000 ? 1 : 2, + notation: Math.abs(value) >= 1000 ? 'compact' : 'standard', + }).format(value); +}; + +const formatAxisNumber = (value: number, culture?: string) => { + return new Intl.NumberFormat(culture || undefined, { + maximumFractionDigits: 2, + }).format(value); +}; + +const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max); + +const getMedian = (values: number[]) => { + if (values.length === 0) { + return 0; + } + const sorted = [...values].sort((left, right) => left - right); + const middle = Math.floor(sorted.length / 2); + return sorted.length % 2 === 0 ? (sorted[middle - 1] + sorted[middle]) / 2 : sorted[middle]; +}; + +const lightenColor = (color: string, ratio: number) => { + const normalized = color.replace('#', ''); + if (normalized.length !== 6) { + return color; + } + const red = parseInt(normalized.slice(0, 2), 16); + const green = parseInt(normalized.slice(2, 4), 16); + const blue = parseInt(normalized.slice(4, 6), 16); + const mix = (channel: number) => Math.round(channel + (255 - channel) * ratio); + return `rgb(${mix(red)}, ${mix(green)}, ${mix(blue)})`; +}; + +const truncateText = (text: string, maxLength: number) => { + return text.length > maxLength ? `${text.slice(0, maxLength - 1)}…` : text; +}; + +const getNiceStep = (start: number, stop: number, count: number) => { + const safeCount = Math.max(count, 1); + const rawStep = Math.abs(stop - start) / safeCount; + const power = Math.floor(Math.log10(rawStep || 1)); + const base = Math.pow(10, power); + const error = rawStep / base; + + if (error >= Math.sqrt(50)) { + return base * 10; + } + if (error >= Math.sqrt(10)) { + return base * 5; + } + if (error >= Math.sqrt(2)) { + return base * 2; + } + return base; +}; + +const getNiceDomainAndTicks = (min: number, max: number, count: number) => { + if (min === max) { + return { domain: [min, max] as [number, number], ticks: [min] }; + } + + const step = getNiceStep(min, max, count); + const domainStart = Math.floor(min / step) * step; + const domainEnd = Math.ceil(max / step) * step; + const ticks: number[] = []; + + for (let value = domainStart; value <= domainEnd + step / 2; value += step) { + ticks.push(Number(value.toFixed(12))); + } + + return { + domain: [domainStart, domainEnd] as [number, number], + ticks, + }; +}; + +const getClosestPairDiffAndRange = (values: number[]) => { + if (values.length < 2) { + return undefined; + } + + const sorted = [...values].sort((left, right) => left - right); + let closestPairDiff = Number.POSITIVE_INFINITY; + + for (let index = 1; index < sorted.length; index++) { + closestPairDiff = Math.min(closestPairDiff, sorted[index] - sorted[index - 1]); + } + + if (!Number.isFinite(closestPairDiff) || closestPairDiff <= 0) { + return undefined; + } + + return [closestPairDiff, sorted[sorted.length - 1] - sorted[0]] as const; +}; + +export class HorizontalBarChartWithAxis extends FASTElement { + @attr({ converter: jsonConverter }) + public data!: HorizontalBarChartWithAxisDataPoint[]; + + @attr({ attribute: 'chart-title' }) + public chartTitle?: string; + + @attr + public width?: number | string; + + @attr({ attribute: 'legend-list-label' }) + public legendListLabel?: string; + + @attr({ attribute: 'hide-legends', converter: booleanStringConverter }) + public hideLegends: boolean = false; + + @attr({ attribute: 'hide-tooltip', converter: booleanStringConverter }) + public hideTooltip: boolean = false; + + @attr({ attribute: 'hide-labels', converter: booleanStringConverter }) + public hideLabels: boolean = false; + + @attr({ attribute: 'show-y-axis-labels', converter: booleanStringConverter }) + public showYAxisLabels: boolean = false; + + @attr({ attribute: 'show-y-axis-labels-tooltip', converter: booleanStringConverter }) + public showYAxisLabelsTooltip: boolean = false; + + @attr({ attribute: 'use-single-color', converter: booleanStringConverter }) + public useSingleColor: boolean = false; + + @attr({ attribute: 'enable-gradient', converter: booleanStringConverter }) + public enableGradient: boolean = false; + + @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + public roundCorners: boolean = false; + + @attr({ attribute: 'allow-multiple-legend-selection', converter: booleanStringConverter }) + public allowMultipleLegendSelection: boolean = false; + + @attr({ attribute: 'bar-height' }) + public barHeight?: number | string; + + @attr({ attribute: 'height' }) + public height?: number | string; + + @attr({ attribute: 'x-axis-tick-count' }) + public xAxisTickCount?: number | string; + + @attr({ attribute: 'y-axis-tick-count' }) + public yAxisTickCount?: number | string; + + @attr({ attribute: 'y-axis-padding' }) + public yAxisPadding?: number | string; + + @attr({ attribute: 'x-min-value' }) + public xMinValue?: number | string; + + @attr({ attribute: 'x-max-value' }) + public xMaxValue?: number | string; + + @attr({ attribute: 'y-min-value' }) + public yMinValue?: number | string; + + @attr({ attribute: 'y-max-value' }) + public yMaxValue?: number | string; + + @attr({ attribute: 'y-axis-category-order' }) + public yAxisCategoryOrder: AxisCategoryOrder = 'default'; + + @attr + public culture?: string; + + @observable + public uniqueLegends: HorizontalBarChartWithAxisLegend[] = []; + + @observable + public activeLegend: string = ''; + + @observable + public selectedLegends: string[] = []; + + @observable + public tooltipProps: TooltipProps = { + isVisible: false, + legend: '', + xLabel: X_AXIS_LABEL, + xValue: '', + yLabel: Y_AXIS_LABEL, + yValue: '', + color: '', + xPos: 0, + yPos: 0, + }; + + public chartContainer!: HTMLDivElement; + public elementInternals: ElementInternals = this.attachInternals(); + + private _renderedBars: RenderedBar[] = []; + private _resizeObserver?: ResizeObserver; + private _isRTL: boolean = false; + + public connectedCallback() { + this._initializeFromAttributes(); + + super.connectedCallback(); + this.elementInternals.role = 'region'; + + this._isRTL = getRTL(this); + this._resizeObserver = new ResizeObserver(() => this._renderChart()); + this._resizeObserver.observe(this); + if (this.data) { + this._renderChart(); + } + } + + public disconnectedCallback() { + this._resizeObserver?.disconnect(); + super.disconnectedCallback(); + } + + public handleLegendMouseoverAndFocus = (legendTitle: string) => { + if (this.selectedLegends.length > 0) { + return; + } + this.activeLegend = legendTitle; + }; + + public handleLegendMouseoutAndBlur = () => { + if (this.selectedLegends.length > 0) { + return; + } + this.activeLegend = ''; + }; + + public handleLegendClick = (legendTitle: string) => { + if (this.allowMultipleLegendSelection) { + const nextSelection = this.selectedLegends.includes(legendTitle) + ? this.selectedLegends.filter(legend => legend !== legendTitle) + : [...this.selectedLegends, legendTitle]; + this.selectedLegends = nextSelection; + if (nextSelection.length === 0) { + this.activeLegend = ''; + } else if (!nextSelection.includes(this.activeLegend)) { + this.activeLegend = nextSelection[nextSelection.length - 1]; + } + return; + } + + if (this.selectedLegends.length === 1 && this.selectedLegends[0] === legendTitle) { + this.selectedLegends = []; + this.activeLegend = ''; + return; + } + + this.selectedLegends = [legendTitle]; + this.activeLegend = legendTitle; + }; + + public isLegendSelected(legendTitle: string) { + return Array.isArray(this.selectedLegends) && this.selectedLegends.includes(legendTitle); + } + + public isLegendDimmed(legendTitle: string) { + const highlighted = this._getHighlightedLegends(); + return highlighted.length > 0 && !highlighted.includes(legendTitle); + } + + protected activeLegendChanged() { + this._applyLegendState(); + } + + protected selectedLegendsChanged() { + this._applyLegendState(); + } + + protected dataChanged() { + this._renderChart(); + } + + protected chartTitleChanged() { + this._renderChart(); + } + + protected widthChanged() { + this._renderChart(); + } + + protected hideLabelsChanged() { + this._renderChart(); + } + + protected useSingleColorChanged() { + this._renderChart(); + } + + protected enableGradientChanged() { + this._renderChart(); + } + + protected roundCornersChanged() { + this._renderChart(); + } + + protected allowMultipleLegendSelectionChanged() { + if (!this.allowMultipleLegendSelection) { + this.selectedLegends = []; + this.activeLegend = ''; + return; + } + + this._applyLegendState(); + } + + protected barHeightChanged() { + this._renderChart(); + } + + protected xAxisTickCountChanged() { + this._renderChart(); + } + + protected yAxisTickCountChanged() { + this._renderChart(); + } + + protected yAxisPaddingChanged() { + this._renderChart(); + } + + protected xMinValueChanged() { + this._renderChart(); + } + + protected xMaxValueChanged() { + this._renderChart(); + } + + protected yMinValueChanged() { + this._renderChart(); + } + + protected yMaxValueChanged() { + this._renderChart(); + } + + protected yAxisCategoryOrderChanged() { + this._renderChart(); + } + + protected showYAxisLabelsChanged() { + this._renderChart(); + } + + protected showYAxisLabelsTooltipChanged() { + this._renderChart(); + } + + public get tooltipInlineTransform() { + return this._isRTL ? 'translateX(50%)' : 'translateX(-50%)'; + } + + private _renderChart() { + if (!this.$fastController.isConnected || !this.chartContainer) { + return; + } + + this._clearChart(); + + if (!Array.isArray(this.data) || this.data.length === 0) { + this.uniqueLegends = []; + this.elementInternals.ariaLabel = this.chartTitle || 'Horizontal bar chart with axis has no data.'; + return; + } + + this._validateData(this.data); + this._isRTL = getRTL(this); + this.elementInternals.ariaLabel = this._getChartAriaLabel(); + this._applyHostDimensions(); + + const width = Math.max(this.getBoundingClientRect().width || 640, 320); + const groups = this._getGroupedSeries(); + const numericYAxis = typeof groups[0]?.rawY === 'number'; + const yValues = groups.map(group => group.rawY).filter((value): value is number => typeof value === 'number'); + const height = this._getChartHeight(groups.length, numericYAxis, yValues); + const yLabelWidth = this._getYAxisLabelWidth(groups, numericYAxis); + const margins = this._isRTL + ? { + top: 20, + right: yLabelWidth, + bottom: 35, + left: 20, + } + : { + top: 20, + right: 20, + bottom: 35, + left: yLabelWidth, + }; + const innerWidth = width - margins.left - margins.right; + const plotLayout = this._getPlotLayout(groups.length, numericYAxis, height, margins, yValues); + const xAxisScale = this._getXScaleInfo(groups); + const yPositionForGroup = this._createYPositioner( + groups, + numericYAxis, + plotLayout.margins, + height, + plotLayout.innerHeight, + yValues, + ); + const svg = createSvgElement('svg'); + + svg.setAttribute('class', 'chart-svg'); + svg.setAttribute('width', `${width}`); + svg.setAttribute('height', `${height}`); + svg.setAttribute('viewBox', `0 0 ${width} ${height}`); + svg.setAttribute('aria-label', this.chartTitle || `Horizontal bar chart with axis with ${this.data.length} bars.`); + + const defs = createSvgElement('defs'); + svg.appendChild(defs); + + const axisLayer = createSvgElement('g'); + const barsLayer = createSvgElement('g'); + svg.appendChild(axisLayer); + svg.appendChild(barsLayer); + + this._renderXAxis(axisLayer, width, height, margins, xAxisScale.domain, xAxisScale.ticks); + this._renderYAxis(axisLayer, groups, numericYAxis, width, height, plotLayout.margins, yPositionForGroup, yValues); + this._renderOriginLine(axisLayer, plotLayout.margins, height, xAxisScale.domain, innerWidth); + + this._renderedBars = []; + const legendColorMap = new Map(); + const scaleX = (value: number) => { + const [min, max] = xAxisScale.domain; + const safeSpan = max - min || 1; + const rangeStart = this._isRTL ? width - margins.right : margins.left; + const rangeEnd = this._isRTL ? margins.left : width - margins.right; + return rangeStart + ((value - min) / safeSpan) * (rangeEnd - rangeStart); + }; + + groups.forEach((group, groupIndex) => { + const yPosition = yPositionForGroup(group, groupIndex); + const resolvedBarHeight = plotLayout.barHeight; + let positiveTotal = 0; + let negativeTotal = 0; + const positivePointCount = group.points.filter(point => point.x >= 0).length; + const negativePointCount = group.points.length - positivePointCount; + let positivePointIndex = 0; + let negativePointIndex = 0; + + group.points.forEach((point, pointIndex) => { + const color = this._getPointColor(point, pointIndex); + const gradientId = this._appendGradient(defs, groupIndex, pointIndex, point, color); + + if (point.legend && !legendColorMap.has(point.legend)) { + legendColorMap.set(point.legend, color); + } + + const startValue = point.x >= 0 ? positiveTotal : negativeTotal; + const endValue = startValue + point.x; + if (point.x >= 0) { + positiveTotal = endValue; + positivePointIndex += 1; + } else { + negativeTotal = endValue; + negativePointIndex += 1; + } + + const xStart = scaleX(startValue); + const xEnd = scaleX(endValue); + const rawWidth = Math.max(Math.abs(xEnd - xStart), 1); + const shouldApplyGap = + rawWidth > STACKED_SEGMENT_GAP && + ((point.x >= 0 && positivePointIndex !== positivePointCount) || + (point.x < 0 && negativePointIndex !== negativePointCount)); + const barWidth = rawWidth - (shouldApplyGap ? STACKED_SEGMENT_GAP : 0); + const rectX = Math.min(xStart, xEnd); + const rect = createSvgElement('rect'); + rect.setAttribute('class', 'bar'); + rect.setAttribute('x', `${rectX}`); + rect.setAttribute('y', `${yPosition - resolvedBarHeight / 2}`); + rect.setAttribute('width', `${barWidth}`); + rect.setAttribute('height', `${resolvedBarHeight}`); + rect.setAttribute('fill', gradientId ? `url(#${gradientId})` : color); + rect.setAttribute('role', 'img'); + rect.setAttribute('tabindex', '0'); + rect.setAttribute('aria-label', this._getAriaLabel(point)); + rect.setAttribute('rx', `${this.roundCorners ? 3 : 0}`); + + rect.addEventListener('mouseover', event => this._showTooltip(point, color, event, rect)); + rect.addEventListener('mouseout', () => this._clearTooltipState()); + rect.addEventListener('focus', event => this._showTooltip(point, color, event, rect)); + rect.addEventListener('blur', () => this._clearTooltipState()); + rect.addEventListener('click', () => point.onClick?.()); + + this._renderedBars.push({ legend: point.legend, element: rect }); + barsLayer.appendChild(rect); + }); + + const totalValue = positiveTotal + negativeTotal; + if (!this.hideLabels && resolvedBarHeight >= 16) { + const label = createSvgElement('text'); + const anchorValue = totalValue >= 0 ? positiveTotal : negativeTotal; + const x = scaleX(anchorValue); + label.setAttribute('class', 'bar-label'); + label.setAttribute('x', `${x}`); + label.setAttribute('y', `${yPosition}`); + label.setAttribute('dominant-baseline', 'central'); + label.setAttribute( + 'text-anchor', + this._isRTL ? (totalValue >= 0 ? 'end' : 'start') : totalValue >= 0 ? 'start' : 'end', + ); + label.setAttribute( + 'transform', + `translate(${totalValue >= 0 ? (this._isRTL ? -4 : 4) : this._isRTL ? 4 : -4}, 0)`, + ); + label.textContent = formatCompactNumber(totalValue, this.culture); + barsLayer.appendChild(label); + } + }); + + this.uniqueLegends = Array.from(legendColorMap.entries()).map(([legend, color]) => ({ legend, color })); + this.chartContainer.appendChild(svg); + this._applyLegendState(); + } + + private _initializeFromAttributes() { + const setString = (name: string, assign: (value: string) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(value); + } + }; + + const setBoolean = (name: string, assign: (value: boolean) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(booleanStringConverter.fromView(value)); + } + }; + + setString('data', value => { this.data = jsonConverter.fromView(value) as HorizontalBarChartWithAxisDataPoint[]; }); + setString('chart-title', value => { this.chartTitle = value; }); + setString('width', value => { this.width = value; }); + setString('legend-list-label', value => { this.legendListLabel = value; }); + setString('bar-height', value => { this.barHeight = value; }); + setString('height', value => { this.height = value; }); + setString('x-axis-tick-count', value => { this.xAxisTickCount = value; }); + setString('y-axis-tick-count', value => { this.yAxisTickCount = value; }); + setString('y-axis-padding', value => { this.yAxisPadding = value; }); + setString('x-min-value', value => { this.xMinValue = value; }); + setString('x-max-value', value => { this.xMaxValue = value; }); + setString('y-min-value', value => { this.yMinValue = value; }); + setString('y-max-value', value => { this.yMaxValue = value; }); + setString('y-axis-category-order', value => { this.yAxisCategoryOrder = value as AxisCategoryOrder; }); + setString('culture', value => { this.culture = value; }); + + setBoolean('hide-legends', value => { this.hideLegends = value; }); + setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); + setBoolean('hide-labels', value => { this.hideLabels = value; }); + setBoolean('show-y-axis-labels', value => { this.showYAxisLabels = value; }); + setBoolean('show-y-axis-labels-tooltip', value => { this.showYAxisLabelsTooltip = value; }); + setBoolean('use-single-color', value => { this.useSingleColor = value; }); + setBoolean('enable-gradient', value => { this.enableGradient = value; }); + setBoolean('round-corners', value => { this.roundCorners = value; }); + setBoolean('allow-multiple-legend-selection', value => { this.allowMultipleLegendSelection = value; }); + } + + private _clearChart() { + if (!this.chartContainer) { + return; + } + this._renderedBars = []; + while (this.chartContainer.firstChild) { + this.chartContainer.removeChild(this.chartContainer.firstChild); + } + } + + private _validateData(data: HorizontalBarChartWithAxisDataPoint[]) { + if (!Array.isArray(data)) { + throw new TypeError('Invalid data: Expected an array.'); + } + + data.forEach((point, index) => { + if (point === null || typeof point !== 'object' || Array.isArray(point)) { + throw new TypeError(`Invalid data[${index}]: Expected an object.`); + } + if (typeof point.x !== 'number') { + throw new TypeError(`Invalid data[${index}].x: Expected a number.`); + } + if (typeof point.y !== 'string' && typeof point.y !== 'number') { + throw new TypeError(`Invalid data[${index}].y: Expected a string or number.`); + } + }); + } + + private _getChartAriaLabel() { + return ( + (this.chartTitle ? `${this.chartTitle}. ` : '') + `Horizontal bar chart with axis with ${this.data.length} bars.` + ); + } + + private _getGroupedSeries(): GroupedSeries[] { + const groups = new Map(); + this.data.forEach(point => { + const key = String(point.y); + const existing = groups.get(key); + if (existing) { + existing.points.push(point); + } else { + groups.set(key, { key, rawY: point.y, points: [point] }); + } + }); + + const groupList = Array.from(groups.values()); + const numericYAxis = typeof groupList[0]?.rawY === 'number'; + if (numericYAxis) { + return groupList.sort((left, right) => Number(right.rawY) - Number(left.rawY)); + } + + return this._sortCategoricalGroups(groupList); + } + + private _sortCategoricalGroups(groups: GroupedSeries[]): GroupedSeries[] { + const order = this.yAxisCategoryOrder || 'default'; + if (order === 'default' || order === 'data') { + const reversed = [...this.data].reverse(); + const orderedKeys = new Set(reversed.map(point => String(point.y))); + return Array.from(orderedKeys) + .map(key => groups.find(group => group.key === key)!) + .filter(Boolean); + } + + const aggregate = (group: GroupedSeries) => { + const values = group.points.map(point => point.x); + switch (order) { + case 'category ascending': + case 'category descending': + return 0; + case 'total ascending': + case 'total descending': + case 'sum ascending': + case 'sum descending': + return values.reduce((sum, value) => sum + value, 0); + case 'min ascending': + case 'min descending': + return Math.min(...values); + case 'max ascending': + case 'max descending': + return Math.max(...values); + case 'mean ascending': + case 'mean descending': + return values.reduce((sum, value) => sum + value, 0) / values.length; + case 'median ascending': + case 'median descending': + return getMedian(values); + default: + return 0; + } + }; + + const sorted = [...groups]; + if (order.startsWith('category')) { + sorted.sort((left, right) => left.key.localeCompare(right.key)); + if (order.endsWith('descending')) { + sorted.reverse(); + } + return sorted; + } + + sorted.sort((left, right) => aggregate(left) - aggregate(right)); + if (order.endsWith('descending')) { + sorted.reverse(); + } + return sorted; + } + + private _getChartHeight(groupCount: number, numericYAxis: boolean, yValues: number[]) { + if (this.height !== undefined) { + return Math.max(toNumber(this.height, DEFAULT_HEIGHT), 160); + } + + if (numericYAxis && yValues.length > 1) { + return DEFAULT_HEIGHT; + } + + return Math.max(DEFAULT_HEIGHT, groupCount * 56 + 56); + } + + private _getPlotLayout( + groupCount: number, + numericYAxis: boolean, + height: number, + baseMargins: { top: number; right: number; bottom: number; left: number }, + yValues: number[], + ): PlotLayout { + const padding = clamp(toNumber(this.yAxisPadding, DEFAULT_Y_AXIS_PADDING), 0, 0.99); + const totalHeight = height - (baseMargins.top + MIN_DOMAIN_MARGIN) - (baseMargins.bottom + MIN_DOMAIN_MARGIN); + let barHeight = this.barHeight !== undefined ? Math.max(toNumber(this.barHeight, DEFAULT_BAR_HEIGHT), 1) : 0; + let domainMargin = MIN_DOMAIN_MARGIN; + + if (numericYAxis) { + if (barHeight === 0) { + barHeight = this._calculateAppropriateNumericBarHeight(yValues, totalHeight, padding); + } + + barHeight = Math.max(barHeight, 1); + domainMargin += barHeight / 2; + } else { + const barGapRate = padding / (1 - padding); + const totalBands = groupCount + Math.max(groupCount - 1, 0) * barGapRate; + if (barHeight === 0) { + barHeight = totalHeight / Math.max(totalBands, 1); + } + + const requiredHeight = totalBands * barHeight; + if (totalHeight >= requiredHeight) { + domainMargin = MIN_DOMAIN_MARGIN + (totalHeight - requiredHeight) / 2; + } + } + + const margins = { + ...baseMargins, + top: baseMargins.top + domainMargin, + bottom: baseMargins.bottom + domainMargin, + }; + + return { + barHeight, + margins, + innerHeight: height - margins.top - margins.bottom, + }; + } + + private _getYAxisLabelWidth(groups: GroupedSeries[], numericYAxis: boolean) { + if (numericYAxis) { + return 40; + } + + const longest = groups.reduce((maxLength, group) => Math.max(maxLength, String(group.rawY).length), 0); + const rawWidth = longest * 7 + 28; + return clamp(rawWidth, 40, this.showYAxisLabels ? 240 : 160); + } + + private _getXScaleInfo(groups: GroupedSeries[]) { + let min = 0; + let max = 0; + + groups.forEach(group => { + const positive = group.points.filter(point => point.x >= 0).reduce((sum, point) => sum + point.x, 0); + const negative = group.points.filter(point => point.x < 0).reduce((sum, point) => sum + point.x, 0); + max = Math.max(max, positive); + min = Math.min(min, negative); + }); + + min = Math.min(min, toOptionalNumber(this.xMinValue) ?? min); + max = Math.max(max, toOptionalNumber(this.xMaxValue) ?? max); + + if (min === max) { + if (max === 0) { + max = 1; + } else { + min = Math.min(0, min); + } + } + + return getNiceDomainAndTicks(min, max, toNumber(this.xAxisTickCount, DEFAULT_X_TICK_COUNT)); + } + + private _getNumericYDomain(yValues: number[]) { + const yMin = Math.min(...yValues); + const yMax = Math.max(...yValues); + const domainMin = Math.min(yMin, toOptionalNumber(this.yMinValue) ?? 0); + const domainMax = Math.max(yMax, toOptionalNumber(this.yMaxValue) ?? 0); + return [domainMin, domainMax] as [number, number]; + } + + private _createYPositioner( + groups: GroupedSeries[], + numericYAxis: boolean, + margins: { top: number; bottom: number }, + height: number, + innerHeight: number, + yValues: number[], + ) { + if (!numericYAxis) { + const padding = clamp(toNumber(this.yAxisPadding, DEFAULT_Y_AXIS_PADDING), 0, 0.95); + const step = innerHeight / Math.max(groups.length + padding, 1); + const barBand = step * (1 - padding); + const startOffset = padding * step; + return (_group: GroupedSeries, index: number) => margins.top + startOffset + index * step + barBand / 2; + } + + const [min, max] = this._getNumericYDomain(yValues); + const safeSpan = max - min || 1; + + return (group: GroupedSeries) => { + const ratio = (Number(group.rawY) - min) / safeSpan; + return height - margins.bottom - ratio * innerHeight; + }; + } + + private _calculateAppropriateNumericBarHeight(yValues: number[], totalHeight: number, innerPadding: number) { + const result = getClosestPairDiffAndRange(yValues); + if (!result || result[1] === 0) { + return 16; + } + + const [closestPairDiff, rawRange] = result; + const yMax = Math.max(...yValues); + const range = Math.max(rawRange, yMax); + return Math.max( + Math.floor((totalHeight * closestPairDiff * (1 - innerPadding)) / (range + closestPairDiff * (1 - innerPadding))), + 1, + ); + } + + private _getPointColor(point: HorizontalBarChartWithAxisDataPoint, index: number) { + if (this.useSingleColor) { + const singleColorPoint = this.data.find( + candidate => typeof candidate.color === 'string' && candidate.color.length > 0, + ); + return singleColorPoint?.color ? getColorFromToken(singleColorPoint.color) : getColorFromToken('qualitative.2'); + } + + if (point.color) { + return getColorFromToken(point.color); + } + + return getNextColor(index, 0); + } + + private _applyHostDimensions() { + if (this.width === undefined || this.width === null || this.width === '') { + this.style.removeProperty('width'); + } else { + this.style.width = this._toCssLength(this.width); + } + + if (this.height === undefined || this.height === null || this.height === '') { + this.style.removeProperty('height'); + } else { + this.style.height = this._toCssLength(this.height); + } + } + + private _toCssLength(value: number | string) { + return typeof value === 'number' || /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value; + } + + private _appendGradient( + defs: SVGDefsElement, + groupIndex: number, + pointIndex: number, + point: HorizontalBarChartWithAxisDataPoint, + color: string, + ) { + if (!this.enableGradient && !point.gradient) { + return undefined; + } + + const gradientId = `hbcwa-gradient-${groupIndex}-${pointIndex}`; + const gradient = createSvgElement('linearGradient'); + gradient.setAttribute('id', gradientId); + gradient.setAttribute('x1', this._isRTL ? '100%' : '0%'); + gradient.setAttribute('x2', this._isRTL ? '0%' : '100%'); + gradient.setAttribute('y1', '0%'); + gradient.setAttribute('y2', '0%'); + + const [from, to] = point.gradient ?? [lightenColor(color, 0.35), color]; + const start = createSvgElement('stop'); + start.setAttribute('offset', '0%'); + start.setAttribute('stop-color', from); + gradient.appendChild(start); + + const end = createSvgElement('stop'); + end.setAttribute('offset', '100%'); + end.setAttribute('stop-color', to); + gradient.appendChild(end); + + defs.appendChild(gradient); + return gradientId; + } + + private _renderXAxis( + axisLayer: SVGGElement, + width: number, + height: number, + margins: { left: number; right: number; bottom: number }, + domain: [number, number], + ticks: number[], + ) { + const axisY = height - margins.bottom; + const min = domain[0]; + const max = domain[1]; + const rangeStart = this._isRTL ? width - margins.right : margins.left; + const rangeEnd = this._isRTL ? margins.left : width - margins.right; + const span = max - min || 1; + const toX = (value: number) => rangeStart + ((value - min) / span) * (rangeEnd - rangeStart); + + ticks.forEach(tick => { + const x = toX(tick); + const tickLine = createSvgElement('line'); + tickLine.setAttribute('class', 'axis-tick-line'); + tickLine.setAttribute('x1', `${x}`); + tickLine.setAttribute('x2', `${x}`); + tickLine.setAttribute('y1', `${axisY}`); + tickLine.setAttribute('y2', `${20}`); + axisLayer.appendChild(tickLine); + + const text = createSvgElement('text'); + text.setAttribute('class', 'axis-text'); + text.setAttribute('x', `${x}`); + text.setAttribute('y', `${axisY + 18}`); + text.setAttribute('text-anchor', 'middle'); + text.textContent = formatAxisNumber(tick, this.culture); + axisLayer.appendChild(text); + }); + } + + private _renderYAxis( + axisLayer: SVGGElement, + groups: GroupedSeries[], + numericYAxis: boolean, + width: number, + height: number, + margins: { top: number; left: number; bottom: number; right: number }, + yPositionForGroup: (group: GroupedSeries, index: number) => number, + yValues: number[], + ) { + const axisX = this._isRTL ? width - margins.right : margins.left; + if (numericYAxis) { + const [min, max] = this._getNumericYDomain(yValues); + const yAxisScale = getNiceDomainAndTicks(min, max, toNumber(this.yAxisTickCount, DEFAULT_Y_TICK_COUNT)); + const safeSpan = yAxisScale.domain[1] - yAxisScale.domain[0] || 1; + yAxisScale.ticks.forEach(tick => { + const ratio = (tick - yAxisScale.domain[0]) / safeSpan; + const y = height - margins.bottom - ratio * (height - margins.top - margins.bottom); + this._appendYAxisTick(axisLayer, axisX, y, formatCompactNumber(tick, this.culture).toLowerCase()); + }); + return; + } + + groups.forEach((group, index) => { + const y = yPositionForGroup(group, index); + const fullLabel = String(group.rawY); + const label = this.showYAxisLabels ? fullLabel : truncateText(fullLabel, 18); + this._appendYAxisTick(axisLayer, axisX, y, label, this.showYAxisLabelsTooltip ? fullLabel : undefined); + }); + } + + private _appendYAxisTick(axisLayer: SVGGElement, axisX: number, y: number, label: string, tooltipText?: string) { + const tickLine = createSvgElement('line'); + tickLine.setAttribute('class', 'axis-tick-line'); + tickLine.setAttribute('x1', `${axisX}`); + tickLine.setAttribute('x2', `${axisX + 6}`); + tickLine.setAttribute('y1', `${y}`); + tickLine.setAttribute('y2', `${y}`); + axisLayer.appendChild(tickLine); + + const text = createSvgElement('text'); + text.setAttribute('class', 'y-axis-text'); + text.setAttribute('x', `${axisX + (this._isRTL ? 12 : -12)}`); + text.setAttribute('y', `${y}`); + text.setAttribute('dominant-baseline', 'central'); + text.setAttribute('text-anchor', 'end'); + text.textContent = label; + + if (tooltipText) { + const title = createSvgElement('title'); + title.textContent = tooltipText; + text.appendChild(title); + } + axisLayer.appendChild(text); + } + + private _renderOriginLine( + axisLayer: SVGGElement, + margins: { top: number; right: number; left: number; bottom: number }, + height: number, + domain: [number, number], + innerWidth: number, + ) { + if (!(domain[0] < 0 && domain[1] > 0)) { + return; + } + + const span = domain[1] - domain[0] || 1; + const rangeStart = this._isRTL + ? this.getBoundingClientRect().width - margins.right || margins.left + innerWidth + : margins.left; + const rangeEnd = this._isRTL + ? margins.left + : this.getBoundingClientRect().width - margins.right || margins.left + innerWidth; + const originX = rangeStart + ((0 - domain[0]) / span) * (rangeEnd - rangeStart); + const line = createSvgElement('line'); + line.setAttribute('class', 'origin-line'); + line.setAttribute('x1', `${originX}`); + line.setAttribute('x2', `${originX}`); + line.setAttribute('y1', `${margins.top}`); + line.setAttribute('y2', `${height - margins.bottom}`); + axisLayer.appendChild(line); + } + + private _showTooltip( + point: HorizontalBarChartWithAxisDataPoint, + color: string, + event: MouseEvent | FocusEvent, + target: SVGRectElement, + ) { + const hostRect = this.getBoundingClientRect(); + const targetRect = target.getBoundingClientRect(); + const xReference = 'clientX' in event ? event.clientX : targetRect.left + targetRect.width / 2; + const xPos = this._isRTL ? hostRect.right - xReference : xReference - hostRect.left; + const yPos = ('clientY' in event ? event.clientY : targetRect.top) - hostRect.top - 44; + this.tooltipProps = { + isVisible: true, + legend: point.legend || '', + xLabel: X_AXIS_LABEL, + xValue: point.xAxisCalloutData || formatAxisNumber(point.x, this.culture), + yLabel: Y_AXIS_LABEL, + yValue: point.yAxisCalloutData || String(point.y), + color, + xPos: Math.max(0, xPos), + yPos: Math.max(0, yPos), + }; + } + + private _clearTooltipState() { + this.tooltipProps = { + isVisible: false, + legend: '', + xLabel: X_AXIS_LABEL, + xValue: '', + yLabel: Y_AXIS_LABEL, + yValue: '', + color: '', + xPos: 0, + yPos: 0, + }; + } + + private _getAriaLabel(point: HorizontalBarChartWithAxisDataPoint) { + const xValue = point.xAxisCalloutData || point.x; + const legend = point.legend; + const yValue = point.yAxisCalloutData || point.y; + return point.callOutAccessibilityData?.ariaLabel || `${yValue}. ${legend ? `${legend}, ` : ''}${xValue}.`; + } + + private _getHighlightedLegends() { + if (Array.isArray(this.selectedLegends) && this.selectedLegends.length > 0) { + return this.selectedLegends; + } + return this.activeLegend ? [this.activeLegend] : []; + } + + private _applyLegendState() { + const highlighted = this._getHighlightedLegends(); + if (!Array.isArray(this._renderedBars)) { + return; + } + + this._renderedBars.forEach(({ legend, element }) => { + const shouldHighlight = highlighted.length === 0 || (legend ? highlighted.includes(legend) : true); + element.classList.toggle('inactive', !shouldHighlight); + element.setAttribute('opacity', shouldHighlight ? '1' : '0.1'); + element.setAttribute('tabindex', shouldHighlight ? '0' : '-1'); + }); + } +} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/index.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/index.ts new file mode 100644 index 0000000000..7ba55e5665 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/index.ts @@ -0,0 +1,4 @@ +export { definition as HorizontalBarChartWithAxisDefinition } from './horizontal-bar-chart-with-axis.definition.js'; +export { HorizontalBarChartWithAxis } from './horizontal-bar-chart-with-axis.js'; +export { styles as HorizontalBarChartWithAxisStyles } from './horizontal-bar-chart-with-axis.styles.js'; +export { template as HorizontalBarChartWithAxisTemplate } from './horizontal-bar-chart-with-axis.template.js'; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/define.ts b/src/Charts.Scripts/src/horizontal-bar-chart/define.ts new file mode 100644 index 0000000000..2af6e03097 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/define.ts @@ -0,0 +1,4 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { definition } from './horizontal-bar-chart.definition.js'; + +definition.define(FluentDesignSystem.registry); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.bench.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.bench.ts new file mode 100644 index 0000000000..14336cd320 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.bench.ts @@ -0,0 +1,12 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { definition } from './horizontal-bar-chart.definition.js'; + +definition.define(FluentDesignSystem.registry); + +const itemRenderer = () => { + const horizontalbarchart = document.createElement('fluent-horizontal-bar-chart'); + return horizontalbarchart; +}; + +export default itemRenderer; +export { tests } from '../utils/benchmark-wrapper.js'; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.definition.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.definition.ts new file mode 100644 index 0000000000..580141fbec --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.definition.ts @@ -0,0 +1,18 @@ +import { FluentDesignSystem } from '@fluentui/web-components'; +import { HorizontalBarChart } from './horizontal-bar-chart.js'; +import { styles } from './horizontal-bar-chart.styles.js'; +import { template } from './horizontal-bar-chart.template.js'; + +/** + * @public + * @remarks + * HTML Element: `` + */ +export const definition = HorizontalBarChart.compose({ + name: `${FluentDesignSystem.prefix}-horizontal-bar-chart`, + template, + styles, + shadowOptions: { + delegatesFocus: true, + }, +}); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.options.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.options.ts new file mode 100644 index 0000000000..13b6150cc6 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.options.ts @@ -0,0 +1,50 @@ +export enum Variant { + PartToWhole = 'part-to-whole', + AbsoluteScale = 'absolute-scale', + SingleBar = 'single-bar', +} + +export interface ChartDataPoint { + /** + * Legend text for the datapoint in the chart + */ + legend: string; + + /** + * data the datapoint in the chart + */ + data: number; + + /** + * total length of bar + */ + total?: number; + + /** + * onClick action for each datapoint in the chart + */ + onClick?: VoidFunction; + + /** + * Color for the legend in the chart. If not provided, it will fallback on the default color palette. + */ + color?: string; + + gradient?: [string, string]; +} + +export interface ChartProps { + /** + * title for the data series + */ + chartSeriesTitle?: string; + + /** + * data for the points in the chart + */ + chartData: ChartDataPoint[]; + + benchmarkData?: number; + + chartDataText?: string; +} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.styles.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.styles.ts new file mode 100644 index 0000000000..32b8d701c9 --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.styles.ts @@ -0,0 +1,185 @@ +import type { ElementStyles } from '@microsoft/fast-element'; +import { css } from '@microsoft/fast-element'; +import { + colorNeutralBackground1, + colorNeutralForeground1, + colorNeutralStrokeAccessible, + display, + forcedColorsStylesheetBehavior, + shadow4, + spacingHorizontalL, + spacingHorizontalNone, + spacingHorizontalS, + spacingHorizontalSNudge, + spacingVerticalL, + spacingVerticalM, + spacingVerticalMNudge, + spacingVerticalNone, + spacingVerticalS, + spacingVerticalXS, + strokeWidthThick, + strokeWidthThickest, + strokeWidthThin, + typographyBody1StrongStyles, + typographyBody1Styles, + typographyCaption1Styles, + typographyTitle2Styles, +} from '@fluentui/web-components'; + +/** + * Styles for the HorizontalBarChart component. + * + * @public + */ +export const styles: ElementStyles = css` + ${display('inline-block')} + + :host { + position: relative; + width: 100%; + } + .tooltip { + ${typographyCaption1Styles} + position: absolute; + z-index: 999; + display: grid; + overflow: hidden; + padding: ${spacingVerticalMNudge} ${spacingHorizontalL}; + backgroundcolor: ${colorNeutralBackground1}; + background-blend-mode: normal, luminosity; + text-align: center; + background: ${colorNeutralBackground1}; + box-shadow: ${shadow4}; + border: ${strokeWidthThick}; + pointer-events: none; + } + .tooltip-line { + padding-inline-start: ${spacingHorizontalS}; + height: 50px; + border-inline-start: ${strokeWidthThickest} solid; + } + .tooltip-legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + text-align: start; + } + .tooltip-data-y { + ${typographyTitle2Styles} + text-align: start; + } + .bar { + opacity: 1; + } + .bar.inactive { + opacity: 0.1; + } + .bar:focus { + outline: none; + stroke-width: ${strokeWidthThick}; + stroke: black; + } + .svg-chart { + display: block; + overflow: visible; + } + .chart-title { + ${typographyBody1StrongStyles} + color: ${colorNeutralForeground1}; + margin-bottom: ${spacingVerticalS}; + } + .bar-title { + ${typographyBody1Styles} + color: ${colorNeutralForeground1}; + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + display: block; + } + .legend-container { + display: flex; + flex-direction: row; + flex-wrap: wrap; + padding-top: ${spacingVerticalL}; + width: 100%; + align-items: center; + margin: -${spacingVerticalS} ${spacingHorizontalNone} ${spacingVerticalNone} -${spacingHorizontalS}; + } + .legend { + display: flex; + align-items: center; + cursor: pointer; + border: none; + padding: ${spacingHorizontalS}; + background: none; + text-transform: capitalize; + } + .legend-rect { + width: 12px; + height: 12px; + margin-inline-end: ${spacingHorizontalS}; + border: ${strokeWidthThin} solid; + } + .legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + } + .legend.inactive .legend-rect { + background-color: transparent !important; + } + .legend.inactive .legend-text { + opacity: 0.67; + } + .bar-label { + ${typographyBody1StrongStyles} + fill: ${colorNeutralForeground1}; + } + .bar-title-div { + width: 100%; + display: flex; + justify-content: space-between; + } + .ratio-numerator { + ${typographyBody1StrongStyles} + color: ${colorNeutralForeground1}; + } + .ratio-denominator { + ${typographyBody1StrongStyles} + color: ${colorNeutralForeground1}; + font-weight: bold; + } + .benchmark-container { + position: relative; + height: 7px; + margin-top: -3px; + } + .triangle { + width: 0; + height: 0; + border-left: ${strokeWidthThickest} solid transparent; + border-right: ${strokeWidthThickest} solid transparent; + border-bottom: 7px solid; + border-bottom-color: ${colorNeutralStrokeAccessible}; + margin-bottom: ${spacingVerticalXS}; + position: absolute; + } + .chart-data-text { + ${typographyBody1StrongStyles} + color: ${colorNeutralForeground1}; + } +`.withBehaviors( + forcedColorsStylesheetBehavior(css` + .legend-rect, + .tooltip-line, + .triangle { + forced-color-adjust: none; + } + .tooltip-legend-text, + .tooltip-content-y { + forced-color-adjust: auto; + color: CanvasText; + } + .bar-label { + fill: CanvasText !important; + } + `), +); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.template.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.template.ts new file mode 100644 index 0000000000..83040ed4fd --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.template.ts @@ -0,0 +1,67 @@ +import { ElementViewTemplate, html, ref, repeat, when } from '@microsoft/fast-element'; +import type { HorizontalBarChart } from './horizontal-bar-chart.js'; +import type { ChartDataPoint } from './horizontal-bar-chart.options.js'; + +/** + * Generates a template for the HorizontalBarChart component. + * + * @public + */ +export function horizontalbarchartTemplate(): ElementViewTemplate { + return html` + + `; +} + +/** + * @internal + */ +export const template: ElementViewTemplate = horizontalbarchartTemplate(); diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts new file mode 100644 index 0000000000..9dea5b158c --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts @@ -0,0 +1,600 @@ +import { attr, FASTElement, observable } from '@microsoft/fast-element'; +import { create as d3Create, select as d3Select } from 'd3-selection'; +import { + getRTL, + jsonConverter, + booleanStringConverter, + SVG_NAMESPACE_URI, + validateChartPropsArray +} from '../utils/chart-helpers.js'; +import type { ChartDataPoint, ChartProps } from './horizontal-bar-chart.options.js'; +import { Variant } from './horizontal-bar-chart.options.js'; + +/** + * A Horizontal Bar Chart HTML Element. + * + * @public + */ +export class HorizontalBarChart extends FASTElement { + @attr + public width?: number | string; + + @attr + public height?: number | string; + + @attr + public variant?: Variant; + + @attr({ converter: jsonConverter }) + public data!: ChartProps[]; + + @attr({ attribute: 'hide-ratio', converter: booleanStringConverter }) + public hideRatio: boolean = false; + + @attr({ attribute: 'hide-labels', converter: booleanStringConverter }) + public hideLabels: boolean = false; + + @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + public roundCorners: boolean = false; + + @attr({ attribute: 'chart-data-mode' }) + public chartDataMode: 'default' | 'fraction' | 'percentage' = 'default'; + + @attr({ attribute: 'hide-legends', converter: booleanStringConverter }) + public hideLegends: boolean = false; + + @attr({ attribute: 'hide-tooltip', converter: booleanStringConverter }) + public hideTooltip: boolean = false; + + @attr({ attribute: 'legend-list-label' }) + public legendListLabel?: string; + + @attr({ attribute: 'chart-title' }) + public chartTitle?: string; + + @observable + public uniqueLegends: ChartDataPoint[] = []; + + @observable + public activeLegend: string = ''; + protected activeLegendChanged = (oldValue: string, newValue: string) => { + if (newValue === '') { + this._bars?.forEach(bar => bar.classList.remove('inactive')); + } else { + this._bars?.forEach(bar => { + if (bar.getAttribute('barinfo') === newValue) { + bar.classList.remove('inactive'); + } else { + bar.classList.add('inactive'); + } + }); + } + }; + + @observable + public isLegendSelected: boolean = false; + + @observable + public tooltipProps = { + isVisible: false, + legend: '', + yValue: '', + color: '', + xPos: 0, + yPos: 0, + }; + + public chartContainer!: HTMLDivElement; + public elementInternals: ElementInternals = this.attachInternals(); + + private _isRTL: boolean = false; + private _barHeight: number = 12; + private _bars: SVGRectElement[] = []; + + constructor() { + super(); + + this.elementInternals.role = 'region'; + } + + public handleLegendMouseoverAndFocus = (legendTitle: string) => { + if (this.isLegendSelected) { + return; + } + + this.activeLegend = legendTitle; + }; + + public handleLegendMouseoutAndBlur = () => { + if (this.isLegendSelected) { + return; + } + + this.activeLegend = ''; + }; + + public handleLegendClick = (legendTitle: string) => { + if (this.isLegendSelected && this.activeLegend === legendTitle) { + this.activeLegend = ''; + this.isLegendSelected = false; + } else { + this.activeLegend = legendTitle; + this.isLegendSelected = true; + } + }; + + connectedCallback() { + this._initializeFromAttributes(); + + super.connectedCallback(); + + if (!this.data) { + return; + } + + this._initializeAll(); + } + + protected dataChanged(_oldValue: ChartProps[], newValue: ChartProps[]) { + if (this.$fastController.isConnected && newValue) { + this._clearChart(); + this._initializeAll(); + } + } + + protected chartTitleChanged() { + if (this.$fastController.isConnected && this.data) { + this._clearChart(); + this._initializeAll(); + } + } + + protected widthChanged() { + if (this.$fastController.isConnected && this.data) { + this._clearChart(); + this._initializeAll(); + } + } + + protected heightChanged() { + if (this.$fastController.isConnected && this.data) { + this._clearChart(); + this._initializeAll(); + } + } + + protected roundCornersChanged() { + if (this.$fastController.isConnected && this.data) { + this._clearChart(); + this._initializeAll(); + } + } + + private _initializeFromAttributes() { + const setString = (name: string, assign: (value: string) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(value); + } + }; + + const setBoolean = (name: string, assign: (value: boolean) => void) => { + const value = this.getAttribute(name); + if (value !== null) { + assign(booleanStringConverter.fromView(value)); + } + }; + + setString('data', value => { this.data = jsonConverter.fromView(value) as ChartProps[]; }); + setString('width', value => { this.width = value; }); + setString('height', value => { this.height = value; }); + setString('variant', value => { this.variant = value as Variant; }); + setString('chart-data-mode', value => { this.chartDataMode = value as 'default' | 'fraction' | 'percentage'; }); + setString('legend-list-label', value => { this.legendListLabel = value; }); + setString('chart-title', value => { this.chartTitle = value; }); + + setBoolean('hide-ratio', value => { this.hideRatio = value; }); + setBoolean('hide-labels', value => { this.hideLabels = value; }); + setBoolean('round-corners', value => { this.roundCorners = value; }); + setBoolean('hide-legends', value => { this.hideLegends = value; }); + setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); + } + + private _clearChart() { + if (this.chartContainer) { + while (this.chartContainer.firstChild) { + this.chartContainer.removeChild(this.chartContainer.firstChild); + } + } + this._bars = []; + } + + private _initializeAll() { + validateChartPropsArray(this.data, 'data'); + + this._isRTL = getRTL(this); + this.elementInternals.ariaLabel = this.chartTitle || `Horizontal bar chart with ${this.data.length} categories.`; + this._applyHostDimensions(); + + this._initializeData(); + this._renderChart(); + } + + private _applyHostDimensions() { + if (this.width === undefined || this.width === null || this.width === '') { + this.style.removeProperty('width'); + } else { + this.style.width = this._toCssLength(this.width); + } + + if (this.height === undefined || this.height === null || this.height === '') { + this.style.removeProperty('height'); + } else { + this.style.height = this._toCssLength(this.height); + } + } + + private _toCssLength(value: number | string) { + return typeof value === 'number' || /^\d+(\.\d+)?$/.test(value) ? `${value}px` : value; + } + + private _initializeData() { + if (this.variant === Variant.SingleBar) { + this._hydrateData(); + } + this._hydrateLegends(); + } + + private _renderChart() { + const chartContainerDiv = d3Select(this.chartContainer); + chartContainerDiv + .selectAll('div') + .data(this.data!) + .enter() + .append('div') + .each((d, i, nodes) => { + this._createSingleChartBars(d, i, nodes); + }); + } + + private _createSingleChartBars(singleChartData: ChartProps, index: number, nodes: any) { + const singleChartBars = this._createBarsAndLegends(singleChartData!, index); + + // create a div element. Loop through chart bars and add to the div as its children + d3Select(nodes[index]) + .attr('key', index) + .attr('id', `_MSBC_bar-${index}`) + .node()! + .appendChild(singleChartBars.node()); + } + + private _hydrateLegends() { + // Create a map to store unique legends + const uniqueLegendsMap = new Map(); + + // Iterate through all chart points and populate the map + for (const dataSeries of this.data) { + for (const point of dataSeries.chartData!) { + if ((point as any).placeholder === true) { + continue; + } + // Check if the legend is already in the map + if (!uniqueLegendsMap.has(point.legend)) { + uniqueLegendsMap.set(point.legend, { + legend: point.legend, + data: point.data, + color: point.gradient ? point.gradient[0] : point.color, + }); + } + } + } + + // Convert the map values back to an array + this.uniqueLegends = Array.from(uniqueLegendsMap.values()); + } + + private _hydrateData() { + this.data!.forEach(({ chartData }) => { + if (chartData!.length === 1) { + const pointData = chartData![0]; + const newEntry = { + legend: '', + data: Math.max(pointData.total! - pointData.data!, 0), + y: pointData.total!, + color: '#edebe9', + placeholder: true, + }; + chartData!.push(newEntry); + } + }); + } + + private _calculateBarSpacing(): number { + const svgWidth = this.getBoundingClientRect().width; + let barSpacing = 0; + const MARGIN_WIDTH_IN_PX = 3; + if (svgWidth) { + const currentBarSpacing = (MARGIN_WIDTH_IN_PX / svgWidth) * 100; + barSpacing = currentBarSpacing; + } + return barSpacing; + } + + private _createBarsAndLegends(data: ChartProps, barNo?: number) { + const _isRTL = this._isRTL; + const _computeLongestBarTotalValue = () => { + let longestBarTotalValue = 0; + this.data!.forEach(({ chartData }) => { + const barTotalValue = chartData!.reduce((acc: number, point: ChartDataPoint) => acc + (point.data ?? 0), 0); + longestBarTotalValue = Math.max(longestBarTotalValue, barTotalValue); + }); + return longestBarTotalValue; + }; + const longestBarTotalValue = _computeLongestBarTotalValue(); + const noOfBars = + data.chartData?.reduce((count: number, point: ChartDataPoint) => (count += (point.data || 0) > 0 ? 1 : 0), 0) || + 1; + const barSpacingInPercent = this._calculateBarSpacing(); + const totalMarginPercent = barSpacingInPercent * (noOfBars - 1); + // calculating starting point of each bar and it's range + const startingPoint: number[] = []; + const barTotalValue = data.chartData!.reduce((acc: number, point: ChartDataPoint) => acc + (point.data ?? 0), 0); + const total = this.variant === Variant.AbsoluteScale ? longestBarTotalValue : barTotalValue; + + let sumOfPercent = 0; + data.chartData!.map((point: ChartDataPoint, index: number) => { + const pointData = point.data ?? 0; + const currValue = (pointData / total) * 100; + let value = currValue ?? 0; + + if (value < 1 && value !== 0) { + value = 1; + } else if (value > 99 && value !== 100) { + value = 99; + } + sumOfPercent += value; + + return sumOfPercent; + }); + + // Include an imaginary placeholder bar with value equal to + // the difference between longestBarTotalValue and barTotalValue + // while calculating sumOfPercent to get correct scalingRatio for absolute-scale variant + if (this.variant === Variant.AbsoluteScale) { + let value = total === 0 ? 0 : ((total - barTotalValue) / total) * 100; + if (value < 1 && value !== 0) { + value = 1; + } else if (value > 99 && value !== 100) { + value = 99; + } + sumOfPercent += value; + } + + /** + * The %age of the space occupied by the margin needs to subtracted + * while computing the scaling ratio, since the margins are not being + * scaled down, only the data is being scaled down from a higher percentage to lower percentage + * Eg: 95% of the space is taken by the bars, 5% by the margins + * Now if the sumOfPercent is 120% -> This needs to be scaled down to 95%, not 100% + * since that's only space available to the bars + */ + + const scalingRatio = sumOfPercent !== 0 ? sumOfPercent / (100 - totalMarginPercent) : 1; + + let prevPosition = 0; + let value = 0; + + const createBars = (g: SVGGElement, point: ChartDataPoint, index: number) => { + const barHeight = 12; + const pointData = point.data ?? 0; + if (index > 0) { + prevPosition += value; + } + value = (pointData / total) * 100 ? (pointData / total) * 100 : 0; + if (value < 1 && value !== 0) { + value = 1 / scalingRatio; + } else if (value > 99 && value !== 100) { + value = 99 / scalingRatio; + } else { + value = value / scalingRatio; + } + + startingPoint.push(prevPosition); + + const gEle = d3Select(g) // 'this' refers to the current 'g' element + .attr('key', index) + .attr('role', 'img') + .attr('aria-label', pointData); + + let gradientId = ''; + if (point.gradient) { + const defs = document.createElementNS(SVG_NAMESPACE_URI, 'defs'); + gEle.node()!.appendChild(defs); + + const linearGradient = document.createElementNS(SVG_NAMESPACE_URI, 'linearGradient'); + defs.appendChild(linearGradient); + gradientId = `gradient-${barNo}-${index}`; + linearGradient.setAttribute('id', gradientId); + + const stop1 = document.createElementNS(SVG_NAMESPACE_URI, 'stop'); + linearGradient.appendChild(stop1); + stop1.setAttribute('offset', '0%'); + stop1.setAttribute('stop-color', point.gradient[0]); + + const stop2 = document.createElementNS(SVG_NAMESPACE_URI, 'stop'); + linearGradient.appendChild(stop2); + stop2.setAttribute('offset', '100%'); + stop2.setAttribute('stop-color', point.gradient[1]); + } + + const rect = gEle + .append('rect') + .attr('key', index) + .attr('id', `${barNo}-${index}`) + .attr('barinfo', `${point.legend}`) + .attr('class', 'bar') + .attr('style', point.gradient ? `fill:url(#${gradientId})` : `fill:${point.color!}`) + .attr('rx', `${this.roundCorners ? 3 : 0}`) + .attr( + 'x', + `${_isRTL + ? 100 - startingPoint[index] - value - barSpacingInPercent * index + : startingPoint[index] + barSpacingInPercent * index + }%`, + ) + .attr('y', 0) + .attr('width', value + '%') + .attr('height', barHeight) + .attr('tabindex', 0); + this._bars.push(rect.node()!); + }; + + const containerDiv = d3Create('div').attr('style', 'position: relative; margin-bottom: var(--spacingVerticalMNudge);'); + + const barTitleDiv = containerDiv.append('div').attr('class', 'bar-title-div'); + barTitleDiv + .append('div') + .append('span') + .attr('class', 'bar-title') + .text(data?.chartSeriesTitle ? data?.chartSeriesTitle : ''); + + const showChartDataText = this.variant !== Variant.AbsoluteScale; + + if (!this.hideLabels && showChartDataText) { + const numData = data!.chartData![0].data ?? 0; + // Compute total: prefer explicit total field, fall back to sum of all bar data + const explicitTotal = data!.chartData![0].total; + const sumTotal = data!.chartData!.reduce((acc: number, p: ChartDataPoint) => acc + (p.data ?? 0), 0); + const barTotal = explicitTotal !== undefined ? explicitTotal : sumTotal; + + if (data.chartDataText) { + const chartTitleRight = document.createElement('div'); + barTitleDiv.node()!.appendChild(chartTitleRight); + chartTitleRight.classList.add('chart-data-text'); + chartTitleRight.textContent = data.chartDataText; + } else if (this.chartDataMode === 'fraction') { + const ratioDiv = barTitleDiv.append('div').attr('role', 'text'); + ratioDiv.append('span').attr('class', 'ratio-numerator').text(numData); + ratioDiv.append('span').attr('class', 'ratio-denominator').text(`/${barTotal}`); + } else if (this.chartDataMode === 'percentage') { + const percentage = barTotal > 0 ? Math.round((numData / barTotal) * 100) : 0; + barTitleDiv + .append('div') + .attr('role', 'text') + .append('span') + .attr('class', 'ratio-numerator') + .text(`${percentage}%`); + } else { + // 'default' mode: show ratio when there are exactly 2 data points and hideRatio is false + const showRatio = !this.hideRatio && data!.chartData!.length === 2; + if (showRatio) { + const ratioDiv = barTitleDiv.append('div').attr('role', 'text'); + ratioDiv.append('span').attr('class', 'ratio-numerator').text(numData); + ratioDiv.append('span').attr('class', 'ratio-denominator').text(`/${barTotal}`); + } + } + } + + const svgDiv = containerDiv.append('div').attr('style', 'display: flex;'); + const svgEle = svgDiv + .append('svg') + .attr('height', 12) + .attr('width', 100 + '%') + .attr('class', 'svg-chart') + .attr( + 'aria-label', + data?.chartSeriesTitle ?? + `Series with ${data.chartData.length}${data.chartData.length > 1 ? ' stacked' : ''} bars.`, + ) + .selectAll('g') + .data(data.chartData!) + .enter() + .append('g') + .each(function (this, d, i) { + createBars(this, d, i); + }) + .on('mouseover', (event, d) => { + if (d && d.hasOwnProperty('placeholder') && (d as any).placeholder === true) { + return; + } + + const bounds = this.getBoundingClientRect(); + const centerX = window.innerWidth / 2; + const xPos = Math.max(0, Math.min(centerX, window.innerWidth)); + + this.tooltipProps = { + isVisible: true, + legend: d.legend, + yValue: `${d.data}`, + color: d.gradient ? d.gradient[0] : d.color!, + xPos: this._isRTL ? bounds.right - event.clientX : Math.min(event.clientX - bounds.left, xPos), + yPos: event.clientY - bounds.top - 40, + }; + }) + .on('mouseout', () => { + this.tooltipProps = { isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }; + }); + + if (this.variant === Variant.AbsoluteScale) { + const showLabel = !this.hideLabels; + const barLabel = barTotalValue; + if (showLabel) { + if (Math.round((startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent) === 100) { + svgDiv + .append('text') + .attr('key', 'text') + .attr('style', 'margin-top: -4.5px; margin-left: 2px;') + .attr('class', 'bar-label') + .attr( + 'x', + `${this._isRTL + ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent + : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent + }%`, + ) + .attr('textAnchor', 'start') + .attr('y', this._barHeight / 2 + 6) + .attr('dominantBaseline', 'central') + .attr('transform', `translate(${this._isRTL ? -4 : 4})`) + .attr('aria-label', `Total: ${barLabel}`) + .attr('role', 'img') + .text(barLabel); + } else { + svgEle + .append('text') + .attr('key', 'text') + .attr('class', 'bar-label') + .attr( + 'x', + `${this._isRTL + ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent + : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent + }%`, + ) + .attr('textAnchor', 'start') + .attr('y', this._barHeight / 2 + 6) + .attr('dominantBaseline', 'central') + .attr('transform', `translate(${this._isRTL ? -4 : 4})`) + .attr('aria-label', `Total: ${barLabel}`) + .attr('role', 'img') + .text(barLabel); + } + } + } + + if (data.benchmarkData) { + const benchmarkContainer = document.createElement('div'); + containerDiv.node()!.appendChild(benchmarkContainer); + benchmarkContainer.classList.add('benchmark-container'); + + const triangle = document.createElement('div'); + benchmarkContainer.appendChild(triangle); + triangle.classList.add('triangle'); + + const benchmarkRatio = (data.benchmarkData / total) * 100; + triangle.style['insetInlineStart'] = `calc(${benchmarkRatio}% - 4px)`; + } + + return containerDiv; + } +} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/index.ts b/src/Charts.Scripts/src/horizontal-bar-chart/index.ts new file mode 100644 index 0000000000..0695516e7d --- /dev/null +++ b/src/Charts.Scripts/src/horizontal-bar-chart/index.ts @@ -0,0 +1,4 @@ +export { definition as HorizontalBarChartDefinition } from './horizontal-bar-chart.definition.js'; +export { HorizontalBarChart } from './horizontal-bar-chart.js'; +export { styles as HorizontalBarChartStyles } from './horizontal-bar-chart.styles.js'; +export { template as HorizontalBarChartTemplate } from './horizontal-bar-chart.template.js'; diff --git a/src/Charts.Scripts/src/index.ts b/src/Charts.Scripts/src/index.ts index 7b53baf960..3b39a65e69 100644 --- a/src/Charts.Scripts/src/index.ts +++ b/src/Charts.Scripts/src/index.ts @@ -1,9 +1,5 @@ import { Microsoft as FluentUIChartComponentsFile } from './FluentUIChartComponents'; - -// Alias import FluentUIComponents = FluentUIChartComponentsFile.FluentUI.Blazor.FluentUIChartComponents; - FluentUIComponents.defineComponents(); - diff --git a/src/Charts.Scripts/src/utils/chart-helpers.ts b/src/Charts.Scripts/src/utils/chart-helpers.ts new file mode 100644 index 0000000000..7aaeda6270 --- /dev/null +++ b/src/Charts.Scripts/src/utils/chart-helpers.ts @@ -0,0 +1,217 @@ +import type { ValueConverter } from '@microsoft/fast-element'; +import { Direction } from '@microsoft/fast-web-utilities'; +import { getDirection } from '@fluentui/web-components'; + +export const jsonConverter: ValueConverter = { + toView(value: any): string { + return JSON.stringify(value); + }, + fromView(value: unknown): any { + if (value === null || value === undefined) { + return value; + } + + if (typeof value !== 'string') { + return value; + } + + return JSON.parse(value); + }, +}; + +export const booleanStringConverter = { + toView(value: boolean): string { + return value ? 'true' : 'false'; + }, + fromView(value: unknown): boolean { + if (value === null || value === undefined) return false; + if (typeof value === 'boolean') return value; + + const normalized = String(value).trim().toLowerCase(); + if (normalized === 'false') return false; + if (normalized === 'true' || normalized === '') return true; + + return true; + } +}; + +type Dict = { [key: string]: any }; + +export const validateChartPropsArray = (obj: any, objName: string) => { + if (!Array.isArray(obj)) { + throw TypeError(`Invalid ${objName}: Expected an array.`); + } + + obj.forEach((item, idx) => { + validateChartProps(item, `${objName}[${idx}]`); + }); +}; + +export const validateChartProps = (obj: any, objName: string) => { + if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) { + throw TypeError(`Invalid ${objName}: Expected an object.`); + } + + if (!Array.isArray(obj.chartData)) { + throw TypeError(`Invalid ${objName}.chartData: Expected an array.`); + } + + (obj.chartData as any[]).forEach((item, idx) => { + if (item === null || typeof item !== 'object' || Array.isArray(item)) { + throw TypeError(`Invalid ${objName}.chartData[${idx}]: Expected an object.`); + } + + if (typeof item.legend !== 'string') { + throw TypeError(`Invalid ${objName}.chartData[${idx}].legend: Expected a string.`); + } + + if (typeof item.data !== 'number') { + throw TypeError(`Invalid ${objName}.chartData[${idx}].data: Expected a number.`); + } + }); +}; + +export const DataVizPalette = { + color1: 'qualitative.1', + color2: 'qualitative.2', + color3: 'qualitative.3', + color4: 'qualitative.4', + color5: 'qualitative.5', + color6: 'qualitative.6', + color7: 'qualitative.7', + color8: 'qualitative.8', + color9: 'qualitative.9', + color10: 'qualitative.10', + color11: 'qualitative.21', + color12: 'qualitative.22', + color13: 'qualitative.23', + color14: 'qualitative.24', + color15: 'qualitative.25', + color16: 'qualitative.26', + color17: 'qualitative.27', + color18: 'qualitative.28', + color19: 'qualitative.29', + info: 'semantic.info', + disabled: 'semantic.disabled', + highError: 'semantic.highError', + error: 'semantic.error', + warning: 'semantic.warning', + success: 'semantic.success', + highSuccess: 'semantic.highSuccess', +}; + +/** + * Key: Color code. + * Value: + * Index 0 - Default color / Color for light theme, + * Index 1 - Color for dark theme + */ +type Palette = { [key: string]: string[] }; + +const QualitativePalette: Palette = { + '1': ['#637cef'], // [cornflower.tint10], + '2': ['#e3008c'], // [hotPink.primary], + '3': ['#2aa0a4'], // [teal.tint20], + '4': ['#9373c0'], // [orchid.tint10], + '5': ['#13a10e'], // [lightGreen.primary], + '6': ['#3a96dd'], // [lightBlue.primary], + '7': ['#ca5010'], // [pumpkin.primary], + '8': ['#57811b'], // [lime.shade20], + '9': ['#b146c2'], // [lilac.primary], + '10': ['#ae8c00'], // [gold.shade10], + '21': ['#4f6bed'], // [cornflower.primary], + '22': ['#ea38a6'], // [hotPink.tint20], + '23': ['#038387'], // [teal.primary], + '24': ['#8764b8'], // [orchid.primary], + '25': ['#11910d'], // [lightGreen.shade10], + '26': ['#3487c7'], // [lightBlue.shade10], + '27': ['#d06228'], // [pumpkin.tint10], + '28': ['#689920'], // [lime.shade10], + '29': ['#ba58c9'], // [lilac.tint10], +}; + +const SemanticPalette: Palette = { + info: ['#015cda'], + disabled: ['#dbdbdb', '#4d4d4d'], // [grey[86], grey[30]] + highError: ['#6e0811', '#cc2635'], // [cranberry.shade30, cranberry.tint10], + error: ['#c50f1f', '#dc626d'], // [cranberry.primary, cranberry.tint30], + warning: ['#f7630c', '#f87528'], // [orange.primary, orange.tint10], + success: ['#107c10', '#54b054'], // [green.primary, green.tint30], + highSuccess: ['#094509', '#218c21'], // [green.shade30, green.tint10], +}; + +const Colors: { [key: string]: Palette } = { + qualitative: QualitativePalette, + semantic: SemanticPalette, +}; + +const QUALITATIVE_COLORS = Object.values(QualitativePalette); +const TOKENS = Object.values(DataVizPalette); + +const getThemeSpecificColor = (colors: string[], isDarkTheme: boolean): string => { + if (colors.length === 0) { + return ''; + } + const colorIdx = Number(isDarkTheme); + if (colorIdx < colors.length) { + return colors[colorIdx]; + } + return colors[0]; +}; + +export const getNextColor = (index: number, offset: number = 0, isDarkTheme: boolean = false): string => { + const colors = QUALITATIVE_COLORS[(index + offset) % QUALITATIVE_COLORS.length]; + return getThemeSpecificColor(colors, isDarkTheme); +}; + +export const getColorFromToken = (token: string, isDarkTheme: boolean = false): string => { + if (TOKENS.indexOf(token) >= 0) { + const [paletteName, colorCode] = token.split('.'); + const colors = Colors[paletteName][colorCode]; + return getThemeSpecificColor(colors, isDarkTheme); + } + return token; +}; + +export const getRTL = (rootNode: HTMLElement): boolean => { + return getDirection(rootNode) === Direction.rtl; +}; + +export const SVG_NAMESPACE_URI = 'http://www.w3.org/2000/svg'; + +export const wrapText = (text: SVGTextElement, width: number) => { + if (!text.textContent) { + return; + } + + const words = text.textContent.split(/\s+/).reverse(); + let word: string | undefined; + let line: string[] = []; + let lineNumber = 0; + const lineHeight = text.getBoundingClientRect().height; + const y = text.getAttribute('y') || '0'; + + text.textContent = null; + + let tspan = document.createElementNS(SVG_NAMESPACE_URI, 'tspan'); + text.appendChild(tspan); + tspan.setAttribute('x', '0'); + tspan.setAttribute('y', y); + tspan.setAttribute('dy', `${lineNumber++ * lineHeight}`); + + while ((word = words.pop())) { + line.push(word); + tspan.textContent = line.join(' ') + ' '; + if (tspan.getComputedTextLength() > width && line.length > 1) { + line.pop(); + tspan.textContent = line.join(' ') + ' '; + line = [word]; + tspan = document.createElementNS(SVG_NAMESPACE_URI, 'tspan'); + text.appendChild(tspan); + tspan.setAttribute('x', '0'); + tspan.setAttribute('y', y); + tspan.setAttribute('dy', `${lineNumber++ * lineHeight}`); + tspan.textContent = word; + } + } +}; diff --git a/src/Charts.Scripts/tsconfig.json b/src/Charts.Scripts/tsconfig.json index b28a85036c..30c59c60a2 100644 --- a/src/Charts.Scripts/tsconfig.json +++ b/src/Charts.Scripts/tsconfig.json @@ -8,6 +8,7 @@ /* Modules */ "module": "ESNext", "moduleResolution": "bundler", + "experimentalDecorators": true, "lib": [ "ESNext", "DOM" ], /* Emit */ "sourceMap": true, /* Create source map files for emitted JavaScript files. */ diff --git a/src/Charts/Charts/DonutChart/FluentDonutChart.razor b/src/Charts/Charts/DonutChart/FluentDonutChart.razor index 99278f2331..ef7a29066a 100644 --- a/src/Charts/Charts/DonutChart/FluentDonutChart.razor +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor @@ -4,11 +4,13 @@ + /// Gets or sets the title of the donut chart, which is typically displayed above the chart to provide context about the data being represented. + ///
+ [Parameter] + public string ChartTitle { get; set; } = string.Empty; + /// /// Gets or sets the data for the donut chart. /// @@ -48,6 +54,14 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration [Parameter] public int? Width { get; set; } + /// + /// Gets or sets a value indicating whether labels are hidden in the + /// component output. + /// Default is true. + /// + [Parameter] + public bool HideLabels { get; set; } = true; + /// /// Gets or sets a value indicating whether legends are hidden in the component output. /// From 2f5bc1ec400d318d35fbb310cb3315430f63a51c Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Thu, 23 Apr 2026 22:10:30 +0200 Subject: [PATCH 08/60] Improve and finalize(?) donut chart, improve bar charts. Add more demos and docs pages --- .github/copilot-instructions.md | 4 + .../Charts/DebugPages/DonutChartDebug.razor | 43 ++ .../Examples/DonutChartHideLegends.razor | 18 + .../Examples/DonutChartRoundedCorners.razor | 29 + .../DonutChartShowLabelsInPercent.razor | 18 + .../Charts/Examples/DonutChartSizing.razor | 30 + .../HorizontalBarChartWithAxisDefault.razor | 50 ++ .../Components/Charts/FluentUIChartsPage.md | 9 +- .../Charts/Pages/FluentDonutChartPage.md | 20 +- .../Pages/FluentHorizontalBarChartWithAxis.md | 10 + src/Charts.Scripts/.npmrc | 2 +- src/Charts.Scripts/package-lock.json | 21 +- src/Charts.Scripts/package.json | 17 +- .../src/donut-chart/donut-chart.options.ts | 22 +- .../src/donut-chart/donut-chart.spec.ts | 636 ++++++++++++++++++ .../src/donut-chart/donut-chart.stories.ts | 320 +++++++++ .../src/donut-chart/donut-chart.styles.ts | 4 + .../src/donut-chart/donut-chart.template.ts | 52 +- .../src/donut-chart/donut-chart.ts | 598 ++++++++++++---- src/Charts.Scripts/src/helpers.stories.ts | 101 +++ .../horizontal-bar-chart-with-axis.ts | 114 +++- .../horizontal-bar-chart.ts | 90 ++- src/Charts/ChartJson.cs | 14 + .../Charts/DonutChart/FluentDonutChart.razor | 10 +- .../DonutChart/FluentDonutChart.razor.cs | 20 + .../FluentHorizontalBarChartWithAxis.razor | 21 + .../FluentHorizontalBarChartWithAxis.razor.cs | 77 +++ ...HorizontalBarChartWithAxisCategoryOrder.cs | 139 ++++ ...rChartWithAxisDataJsonSerializerContext.cs | 16 + .../HorizontalBarChartWithAxisModels.cs | 96 +++ 30 files changed, 2336 insertions(+), 265 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts create mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts create mode 100644 src/Charts.Scripts/src/helpers.stories.ts create mode 100644 src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor create mode 100644 src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs create mode 100644 src/Charts/Enums/HorizontalBarChartWithAxisCategoryOrder.cs create mode 100644 src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs create mode 100644 src/Charts/Models/HorizontalBarChartWithAxisModels.cs diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..d1a8453e0f --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,4 @@ +# Copilot Instructions + +## Project Guidelines +- Before debugging UI issues with Playwright, verify the Playwright connection first; if it is unavailable, close the browser or tab and retry before proceeding. diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor new file mode 100644 index 0000000000..44998f9323 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor @@ -0,0 +1,43 @@ +@page "/Charts/DonutChart/Debug" +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + + + + +
+ +
+ +
+ + + + +@code { + private readonly DonutChartData data = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + new DonutChartDataPoint { Legend = "third", Data = 15000 }, + ] + }; + + private bool roundCorners; + private bool hideLabels = true; + private bool hideLegends; + private bool showLabelsInPercent; + private bool allowMultipleLegendSelection; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor new file mode 100644 index 0000000000..ba526ddaf9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor @@ -0,0 +1,18 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + +@code { + private readonly DonutChartData data = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor new file mode 100644 index 0000000000..29e1e3be1f --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor @@ -0,0 +1,29 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ Rounded corners + Hide labels +
+ +
+ +
+ +@code { + private readonly DonutChartData data = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + ] + }; + + private bool roundCorners; + private bool hideLabels; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor new file mode 100644 index 0000000000..21c2f57da7 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor @@ -0,0 +1,18 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + +@code { + private readonly DonutChartData data = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor new file mode 100644 index 0000000000..6151ca00a9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor @@ -0,0 +1,30 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + + + + + +@code { + private int _width = 320; + private int _height = 320; + private int _innerRadius = 55; + + private readonly DonutChartData data = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 } + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor new file mode 100644 index 0000000000..40074201d1 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor @@ -0,0 +1,50 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { + X = 10000, + Y = "5000", + Legend = "Oranges", + YAxisCalloutData = "2020/04/30", + XAxisCalloutData = "10%", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 20000, + Y = "50000", + Legend = "Dogs", +YAxisCalloutData = "2020/04/30", + XAxisCalloutData = "20%", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 25000, + Y = "30000", + Legend = "Apples", +YAxisCalloutData = "2020/04/30", + XAxisCalloutData = "37%", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 40000, + Y = "13000", + Legend = "Bananas", + YAxisCalloutData = "2020/04/30", + XAxisCalloutData = "88%", + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md index d070ba09d6..68a617f275 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md @@ -10,9 +10,6 @@ The Fluent UI Charts are a set of components that allow you to easily create cha Currently, the following chart types are available: -- Donut Chart -- Horizontal Bar Chart - -## Examples - -{{ DonutChartDefault }} +- [Donut Chart](/Charts/DonutChart) +- [Horizontal Bar Chart](/Charts/HorizontalBarChart) +- [Horizontal Bar Chart with Axis](/Charts/HorizontalBarChartWithAxis) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md index e22c320d6f..0ec4e494a5 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -38,14 +38,30 @@ Donut charts are used to show proportion, which expresses a partial value in com ## Examples -### Default Donut Chart +### Basic example {{ DonutChartDefault }} -### Donut Chart with labels +### With labels {{ DonutChartLabels }} +### With labels as percentages + +{{ DonutChartShowLabelsInPercent }} + +### Without legends + +{{ DonutChartHideLegends }} + +### Rounded corners + +{{ DonutChartRoundedCorners }} + +### With custom sizing + +{{ DonutChartSizing }} + ### RTL Donut Chart {{ DonutChartDefaultRTL }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md new file mode 100644 index 0000000000..320b190e6d --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md @@ -0,0 +1,10 @@ +--- +title: Horizontal Bar Chart With Axis +route: /Charts/HorizontalBarChartWithAxis +--- + +# Horizontal Bar Chart With Axis + +A horizontal bar chart with axis is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent, along with an axis to provide context for the values. This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long, while also providing a clear reference point for the values being compared. + +{{ HorizontalBarChartWithAxisDefault }} diff --git a/src/Charts.Scripts/.npmrc b/src/Charts.Scripts/.npmrc index 642288e0b9..a8f33531f6 100644 --- a/src/Charts.Scripts/.npmrc +++ b/src/Charts.Scripts/.npmrc @@ -1,3 +1,3 @@ -registry=http://localhost:4873/ +registry=https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ always-auth=true diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json index 4e0a8c64d0..bf6505e3f1 100644 --- a/src/Charts.Scripts/package-lock.json +++ b/src/Charts.Scripts/package-lock.json @@ -8,26 +8,21 @@ "name": "microsoft.fluentui.aspnetcore.components.charts.assets", "license": "ISC", "dependencies": { - "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "3.0.0-rc.13", + "@fluentui/web-components": "^3.0.0-rc.13", "@microsoft/fast-web-utilities": "^6.0.0", - "@types/d3-format": "^3.0.0", - "@types/d3-selection": "^3.0.0", - "@types/d3-shape": "^3.0.0", "d3-format": "^3.0.0", "d3-selection": "^3.0.0", - "d3-shape": "^3.0.0", - "tslib": "^2.1.0" + "d3-shape": "^3.0.0" }, "devDependencies": { - "@microsoft/fast-element": "2.0.0", + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0", + "@types/d3-format": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", "glob": "^13.0.6", "rimraf": "6.1.3" - }, - "peerDependencies": { - "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" } }, "node_modules/@esbuild/aix-ppc64": { @@ -527,24 +522,28 @@ "version": "3.0.4", "resolved": "http://localhost:4873/@types/d3-format/-/d3-format-3.0.4.tgz", "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-path": { "version": "3.1.1", "resolved": "http://localhost:4873/@types/d3-path/-/d3-path-3.1.1.tgz", "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", "resolved": "http://localhost:4873/@types/d3-selection/-/d3-selection-3.0.11.tgz", "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "dev": true, "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.8", "resolved": "http://localhost:4873/@types/d3-shape/-/d3-shape-3.1.8.tgz", "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "dev": true, "license": "MIT", "dependencies": { "@types/d3-path": "*" diff --git a/src/Charts.Scripts/package.json b/src/Charts.Scripts/package.json index f53b7c24c8..5815b8451d 100644 --- a/src/Charts.Scripts/package.json +++ b/src/Charts.Scripts/package.json @@ -20,25 +20,20 @@ "license": "ISC", "type": "module", "devDependencies": { - "@microsoft/fast-element": "2.0.0", + "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0", + "@types/d3-format": "^3.0.0", + "@types/d3-selection": "^3.0.0", + "@types/d3-shape": "^3.0.0", "esbuild": "0.27.4", "esbuild-plugin-inline-css": "0.0.1", "glob": "^13.0.6", "rimraf": "6.1.3" }, "dependencies": { - "@fluentui/tokens": "^1.0.0-alpha.23", - "@fluentui/web-components": "3.0.0-rc.13", + "@fluentui/web-components": "^3.0.0-rc.13", "@microsoft/fast-web-utilities": "^6.0.0", - "@types/d3-format": "^3.0.0", - "@types/d3-selection": "^3.0.0", - "@types/d3-shape": "^3.0.0", "d3-format": "^3.0.0", "d3-selection": "^3.0.0", - "d3-shape": "^3.0.0", - "tslib": "^2.1.0" - }, - "peerDependencies": { - "@microsoft/fast-element": "^2.0.0-beta.26 || ^2.0.0" + "d3-shape": "^3.0.0" } } diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts index 484281fd04..cbd4686c7f 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts @@ -15,13 +15,29 @@ export interface ChartDataPoint { color?: string; /** - * Callout data shown in the tooltip and the center label when the segment is highlighted. - * If not provided, the numeric data value is used. + * Callout data for x axis + * This is an optional prop, If haven;t given legend will take + */ + xAxisCalloutData?: string; + + /** + * Callout data for y axis + * This is an optional prop, If haven't given data will take + */ + yAxisCalloutData?: string; + + /** + * Callout data shown in the center label when the segment is highlighted. */ calloutData?: string; } export interface ChartProps { + /** + * chart title for the chart + */ + chartTitle?: string; + /** * data for the points in the chart */ @@ -29,6 +45,6 @@ export interface ChartProps { } export type Legend = { - legend: string; + title: string; color: string; }; diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts new file mode 100644 index 0000000000..8306a4a247 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts @@ -0,0 +1,636 @@ +import { test } from '@playwright/test'; +import { expect, fixtureURL } from '../helpers.tests.js'; +import type { DonutChart as FluentDonutChart } from './donut-chart.js'; +import type { ChartDataPoint, ChartProps } from './donut-chart.options.js'; + +const basicTitle = 'Donut chart basic example'; + +const points: ChartDataPoint[] = [ + { + legend: 'first', + data: 20000, + }, + { + legend: 'second', + data: 39000, + }, +]; + +const data: ChartProps = { + chartData: points, +}; + +test.describe('Donut-chart - Basic', () => { + test.beforeEach(async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + }); + + test('Should render chart properly', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const legends = element.locator('.legend-text'); + await expect(legends.nth(0).getByText('first')).toBeVisible(); + await expect(legends.nth(1).getByText('second')).toBeVisible(); + await expect(element.getByText('39,000')).toBeVisible(); + await expect(element.locator('.arc-label')).toHaveCount(0); + }); + + test('Should render path with proper attributes and css', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const arcList = element.locator('.arc'); + await expect(arcList).toHaveCount(2); + await expect(arcList.nth(0)).toHaveAttribute('fill', '#637cef'); + await expect(arcList.nth(0)).toHaveAttribute('aria-label', 'first, 20000.'); + await expect(arcList.nth(0)).toHaveAttribute( + 'd', + 'M-76.547,47.334A90,90,0,0,1,-1.055,-89.994L-1.055,-54.99A55,55,0,0,0,-46.993,28.577Z', + ); + await expect(arcList.nth(0)).toHaveCSS('fill', 'rgb(99, 124, 239)'); + await expect(arcList.nth(0)).toHaveCSS('--borderRadiusMedium', '4px'); + + await expect(arcList.nth(1)).toHaveAttribute('fill', '#e3008c'); + await expect(arcList.nth(1)).toHaveAttribute('aria-label', 'second, 39000.'); + await expect(arcList.nth(1)).toHaveAttribute( + 'd', + 'M1.055,-89.994A90,90,0,1,1,-75.417,49.115L-45.863,30.358A55,55,0,1,0,1.055,-54.99Z', + ); + await expect(arcList.nth(1)).toHaveCSS('fill', 'rgb(227, 0, 140)'); + await expect(arcList.nth(1)).toHaveCSS('--borderRadiusMedium', '4px'); + }); + + test('Should render legends data properly', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const legends = element.getByRole('option'); + await expect(legends).toHaveCount(2); + const firstLegend = element.getByRole('option', { name: 'First' }); + const secondLegend = element.getByRole('option', { name: 'Second' }); + await expect(firstLegend).toBeVisible(); + await expect(firstLegend).toHaveText('first'); + await expect(firstLegend).toHaveCSS('--borderRadiusMedium', '4px'); + await expect(secondLegend).toBeVisible(); + await expect(secondLegend).toHaveText('second'); + await expect(secondLegend).toHaveCSS('--borderRadiusMedium', '4px'); + }); + + test('Should update path css values with mouse click event on legend', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const firstLegend = element.getByRole('option', { name: 'First' }); + //mouse events + await firstLegend.click(); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '0.1'); + await firstLegend.dispatchEvent('mouseout'); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '0.1'); + await firstLegend.click(); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + }); + + test('Should remove inactive arcs from the tab order when a legend is active', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const firstLegend = element.getByRole('option', { name: 'First' }); + + await expect(firstPath).toHaveAttribute('tabindex', '0'); + await expect(secondPath).toHaveAttribute('tabindex', '0'); + + await firstLegend.dispatchEvent('mouseover'); + + await expect(firstPath).toHaveAttribute('tabindex', '0'); + await expect(secondPath).toHaveAttribute('tabindex', '-1'); + + await firstLegend.dispatchEvent('mouseout'); + + await expect(firstPath).toHaveAttribute('tabindex', '0'); + await expect(secondPath).toHaveAttribute('tabindex', '0'); + }); + + test('Should update path css values with mouse hover event on legend', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const firstLegend = element.getByRole('option', { name: 'First' }); + //mouse events + await firstLegend.dispatchEvent('mouseover'); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '0.1'); + await firstLegend.dispatchEvent('mouseout'); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + }); + + test('Should show callout with mouse hover event on path', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const calloutRoot = element.locator('.tooltip'); + await expect(calloutRoot).toHaveCount(0); + await firstPath.dispatchEvent('mouseover'); + await expect(calloutRoot).toHaveCount(1); + await expect(calloutRoot).toHaveCSS('opacity', '1'); + const calloutLegendText = element.locator('.tooltip-legend-text'); + await expect(calloutLegendText).toHaveText('first'); + const calloutContentY = element.locator('.tooltip-content-y'); + await expect(calloutContentY).toHaveText('20000'); + await firstPath.dispatchEvent('mouseout'); + await expect(calloutRoot).not.toHaveCSS('opacity', '0'); + }); + + test('Should update callout data when mouse moved from one path to another path', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const calloutRoot = element.locator('.tooltip'); + await expect(calloutRoot).toHaveCount(0); + await firstPath.dispatchEvent('mouseover'); + await expect(calloutRoot).toHaveCSS('opacity', '1'); + const calloutLegendText = element.locator('.tooltip-legend-text'); + await expect(calloutLegendText).toHaveText('first'); + const calloutContentY = element.locator('.tooltip-content-y'); + await expect(calloutContentY).toHaveText('20000'); + const secondPath = element.getByLabel('second,'); + await secondPath.dispatchEvent('mouseover'); + await expect(calloutRoot).toHaveCSS('opacity', '1'); + await expect(calloutLegendText).toHaveText('second'); + await expect(calloutContentY).toHaveText('39000'); + }); +}); + +test.describe('Donut-chart - Reactive rerender', () => { + test('Should rerender when data attribute changes after initial render', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await expect(element.locator('.arc')).toHaveCount(2); + + const newData: ChartProps = { + chartData: [ + { legend: 'alpha', data: 10000 }, + { legend: 'beta', data: 20000 }, + { legend: 'gamma', data: 30000 }, + ], + }; + + await element.evaluate((el, d) => { + el.setAttribute('chart-title', 'Updated chart'); + el.setAttribute('data', JSON.stringify(d)); + }, newData); + + await expect(element.locator('.arc')).toHaveCount(3); + await expect(element.locator('.legend-text').nth(0).getByText('alpha')).toBeVisible(); + await expect(element.locator('.legend-text').nth(1).getByText('beta')).toBeVisible(); + await expect(element.locator('.legend-text').nth(2).getByText('gamma')).toBeVisible(); + }); +}); + +test.describe('Donut-chart - hide-labels', () => { + test('Should keep center text visible and hide outside labels when hide-labels is set', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await expect(element.locator('.text-inside-donut')).toHaveCount(1); + await expect(element.locator('.arc-label')).toHaveCount(0); + await expect(element.locator('.arc')).toHaveCount(2); + }); + + test('Should show outside labels when hide-labels is false', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const firstArc = element.locator('.arc').first(); + const defaultPath = await firstArc.getAttribute('d'); + + await element.evaluate(el => { + (el as FluentDonutChart).hideLabels = false; + }); + + await expect(element.locator('.text-inside-donut')).toHaveCount(1); + await expect(element.locator('.arc-label')).toHaveCount(2); + await expect(firstArc).toHaveAttribute('d', defaultPath ?? ''); + }); + + test('Should react to hide-labels string attribute updates', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await expect(element.locator('.arc-label')).toHaveCount(2); + + await element.evaluate(el => { + el.setAttribute('hide-labels', 'true'); + }); + + await expect(element.locator('.arc-label')).toHaveCount(0); + }); +}); + +test.describe('Donut-chart - outside labels', () => { + test('Should render outside labels for visible segments', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await element.evaluate(el => { + (el as FluentDonutChart).hideLabels = false; + }); + const labels = element.locator('.arc-label'); + + await expect(labels).toHaveCount(2); + await expect(labels.nth(0)).toContainText('20'); + await expect(labels.nth(1)).toContainText('39'); + }); + + test('Should render percent outside labels when show-labels-in-percent is set', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await element.evaluate(el => { + (el as FluentDonutChart).hideLabels = false; + }); + const labels = element.locator('.arc-label'); + await expect(labels).toHaveCount(2); + await expect(labels.nth(0)).toContainText('%'); + await expect(labels.nth(1)).toContainText('%'); + }); + + test('Should react to show-labels-in-percent string attribute updates', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const labels = element.locator('.arc-label'); + await expect(labels).toHaveCount(2); + await expect(labels.nth(0)).not.toContainText('%'); + + await element.evaluate(el => { + el.setAttribute('show-labels-in-percent', 'true'); + }); + + await expect(labels.nth(0)).toContainText('%'); + await expect(labels.nth(1)).toContainText('%'); + }); +}); + +test.describe('Donut-chart - hide-tooltip', () => { + test('Should react to hide-tooltip string attribute updates', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + + await firstPath.dispatchEvent('mouseover'); + await expect(element.locator('.tooltip')).toHaveCount(1); + + await element.evaluate(el => { + el.setAttribute('hide-tooltip', 'true'); + }); + + await expect(element.locator('.tooltip')).toHaveCount(0); + }); +}); + +test.describe('Donut-chart - hide-legends', () => { + test('Should react to hide-legends string attribute updates', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const legendContainer = element.locator('.legend-container'); + await expect(legendContainer).toHaveCount(1); + await expect(legendContainer).not.toBeHidden(); + + await element.evaluate(el => { + el.setAttribute('hide-legends', 'true'); + }); + + await expect(legendContainer).toBeHidden(); + }); +}); + +test.describe('Donut-chart - allow-multiple-legend-selection', () => { + const multiData: ChartProps = { + chartData: [ + { legend: 'first', data: 20000 }, + { legend: 'second', data: 39000 }, + { legend: 'third', data: 15000 }, + ], + }; + + test.beforeEach(async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + }); + + test('Should highlight multiple arcs when multiple legends are selected', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const thirdPath = element.getByLabel('third,'); + const firstLegend = element.getByRole('option', { name: 'first' }); + const secondLegend = element.getByRole('option', { name: 'second' }); + + await firstLegend.click(); + await secondLegend.click(); + + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + await expect(thirdPath).toHaveCSS('opacity', '0.1'); + }); + + test('Should deselect a legend on second click in multi-select mode', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const thirdPath = element.getByLabel('third,'); + const firstLegend = element.getByRole('option', { name: 'first' }); + const secondLegend = element.getByRole('option', { name: 'second' }); + + await firstLegend.click(); + await secondLegend.click(); + await firstLegend.click(); // deselect first + + await expect(firstPath).toHaveCSS('opacity', '0.1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + await expect(thirdPath).toHaveCSS('opacity', '0.1'); + }); + + test('Should restore all arcs when all selections are cleared', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const thirdPath = element.getByLabel('third,'); + const firstLegend = element.getByRole('option', { name: 'first' }); + + await firstLegend.click(); + await firstLegend.click(); // deselect — all clear + + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + await expect(thirdPath).toHaveCSS('opacity', '1'); + }); + + test('Should set aria-selected on selected legends', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstLegend = element.getByRole('option', { name: 'first' }); + const secondLegend = element.getByRole('option', { name: 'second' }); + const thirdLegend = element.getByRole('option', { name: 'third' }); + + await firstLegend.click(); + await secondLegend.click(); + + await expect(firstLegend).toHaveAttribute('aria-selected', 'true'); + await expect(secondLegend).toHaveAttribute('aria-selected', 'true'); + await expect(thirdLegend).toHaveAttribute('aria-selected', 'false'); + }); + + test('Should fall back to single-select when allow-multiple-legend-selection is removed', async ({ page }) => { + const element = page.locator('fluent-donut-chart'); + const firstPath = element.getByLabel('first,'); + const secondPath = element.getByLabel('second,'); + const firstLegend = element.getByRole('option', { name: 'first' }); + const secondLegend = element.getByRole('option', { name: 'second' }); + + await firstLegend.click(); + await secondLegend.click(); + + // disable multi-select → selectedLegends should be cleared + await element.evaluate(el => { + el.removeAttribute('allow-multiple-legend-selection'); + }); + + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '1'); + + // now single-select should work + await firstLegend.click(); + await expect(firstPath).toHaveCSS('opacity', '1'); + await expect(secondPath).toHaveCSS('opacity', '0.1'); + }); +}); + +test.describe('Donut-chart - round-corners', () => { + test('Should change arc geometry when round-corners is enabled', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const firstArc = element.locator('.arc').first(); + const defaultPath = await firstArc.getAttribute('d'); + + await element.evaluate(el => { + el.setAttribute('round-corners', 'true'); + }); + + await expect(firstArc).not.toHaveAttribute('d', defaultPath ?? ''); + }); +}); + +test.describe('Donut-chart - order', () => { + const unorderedData: ChartProps = { + chartData: [ + { legend: 'small', data: 5000 }, + { legend: 'large', data: 39000 }, + { legend: 'medium', data: 15000 }, + ], + }; + + test('Should render legends in default order when order is not set', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const legends = element.locator('.legend-text'); + await expect(legends.nth(0).getByText('small')).toBeVisible(); + await expect(legends.nth(1).getByText('large')).toBeVisible(); + await expect(legends.nth(2).getByText('medium')).toBeVisible(); + }); + + test('Should render legends in sorted (descending) order when order="sorted"', async ({ page }) => { + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + const legends = element.locator('.legend-text'); + // Sorted descending: large (39000), medium (15000), small (5000) + await expect(legends.nth(0).getByText('large')).toBeVisible(); + await expect(legends.nth(1).getByText('medium')).toBeVisible(); + await expect(legends.nth(2).getByText('small')).toBeVisible(); + }); + + test('uses chart-title attr and calloutData for highlighted center text', async ({ page }) => { + const calloutData: ChartProps = { + chartData: [ + { legend: 'first', data: 20000, calloutData: '20K highlighted' }, + { legend: 'second', data: 39000 }, + ], + }; + + await page.goto(fixtureURL('components-donutchart--basic')); + await page.setContent(/* html */ ` +
+ + +
+ `); + await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); + + const element = page.locator('fluent-donut-chart'); + await expect(element.getByText('Callout contract')).toBeVisible(); + + const firstPath = element.getByLabel('first,'); + await firstPath.dispatchEvent('mouseover'); + await expect(element.locator('.text-inside-donut')).toContainText('20K highlighted'); + }); +}); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts new file mode 100644 index 0000000000..8e63f098b8 --- /dev/null +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts @@ -0,0 +1,320 @@ +import { html } from '@microsoft/fast-element'; +import { + FieldDefinition, + FluentDesignSystem, + LabelDefinition, + SliderDefinition, + SwitchDefinition, +} from '@fluentui/web-components'; +import type { Meta, Story, StoryArgs } from '../helpers.stories.js'; +import { renderComponent } from '../helpers.stories.js'; +import { DonutChart as FluentDonutChart } from './donut-chart.js'; +import type { ChartDataPoint, ChartProps } from './donut-chart.options.js'; + +type FluentSliderElement = HTMLElement & { value: string }; +type FluentSwitchElement = HTMLElement & { checked: boolean }; + +const ensureDefinition = (tagName: string, define: () => void) => { + if (!customElements.get(tagName)) { + define(); + } +}; + +ensureDefinition('fluent-field', () => FieldDefinition.define(FluentDesignSystem.registry)); +ensureDefinition('fluent-label', () => LabelDefinition.define(FluentDesignSystem.registry)); +ensureDefinition('fluent-slider', () => SliderDefinition.define(FluentDesignSystem.registry)); +ensureDefinition('fluent-switch', () => SwitchDefinition.define(FluentDesignSystem.registry)); + +const controlsRowStyle = 'display:flex;flex-wrap:wrap;gap:16px 24px;align-items:end;'; +const sliderFieldStyle = 'min-width:220px;flex:1 1 220px;'; +const toggleFieldStyle = 'min-width:220px;'; + +const createSliderField = ( + labelText: string, + id: string, + value: number, + min: number, + max: number, + onChange: (nextValue: number) => void, +) => { + const field = document.createElement('fluent-field'); + field.setAttribute('label-position', 'above'); + field.setAttribute('style', sliderFieldStyle); + + const label = document.createElement('label'); + label.slot = 'label'; + label.htmlFor = id; + label.textContent = labelText; + field.appendChild(label); + + const slider = document.createElement('fluent-slider') as FluentSliderElement; + slider.slot = 'input'; + slider.id = id; + slider.setAttribute('min', `${min}`); + slider.setAttribute('max', `${max}`); + slider.value = `${value}`; + slider.setAttribute('value', `${value}`); + field.appendChild(slider); + + const message = document.createElement('fluent-label'); + message.slot = 'message'; + message.textContent = `${value}`; + field.appendChild(message); + + slider.addEventListener('change', () => onChange(Number(slider.value))); + + return { + element: field, + setValue: (nextValue: number) => { + slider.value = `${nextValue}`; + slider.setAttribute('value', `${nextValue}`); + message.textContent = `${nextValue}`; + }, + }; +}; + +const createSwitchField = ( + labelText: string, + id: string, + checked: boolean, + onChange: (nextChecked: boolean) => void, +) => { + const field = document.createElement('fluent-field'); + field.setAttribute('label-position', 'after'); + field.setAttribute('style', toggleFieldStyle); + + const label = document.createElement('label'); + label.slot = 'label'; + label.htmlFor = id; + label.textContent = labelText; + field.appendChild(label); + + const control = document.createElement('fluent-switch') as FluentSwitchElement; + control.slot = 'input'; + control.id = id; + control.checked = checked; + control.toggleAttribute('checked', checked); + control.addEventListener('change', () => onChange(control.checked)); + field.appendChild(control); + + return { + element: field, + setValue: (nextChecked: boolean) => { + control.checked = nextChecked; + control.toggleAttribute('checked', nextChecked); + }, + }; +}; + +const basicTitle = 'Donut chart basic example'; +const sortedTitle = 'Sorted donut chart example'; + +const points: ChartDataPoint[] = [ + { + legend: 'first', + data: 20000, + }, + { + legend: 'second', + data: 39000, + }, +]; + +const data: ChartProps = { + chartData: points, +}; + +const sortedPoints: ChartDataPoint[] = [ + { + legend: 'small', + data: 5000, + }, + { + legend: 'large', + data: 39000, + }, + { + legend: 'medium', + data: 15000, + }, +]; + +const sortedData: ChartProps = { + chartData: sortedPoints, +}; + +const storyTemplate = html>` + + +`; + +export default { + title: 'Components/DonutChart', +} as Meta; + +export const Basic: Story = renderComponent(storyTemplate).bind({}); + +export const OutsideLabels: Story = () => { + const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; + chart.setAttribute('chart-title', 'Donut chart outside labels example'); + chart.setAttribute('data', JSON.stringify(data)); + chart.setAttribute('value-inside-donut', '39,000'); + chart.setAttribute('inner-radius', '85'); + chart.setAttribute('width', '320'); + chart.setAttribute('height', '320'); + chart.setAttribute('style', 'width:320px;height:320px'); + chart.hideLabels = false; + + return chart; +}; + +export const Sizing: Story = () => { + const container = document.createElement('div'); + const controls = document.createElement('div'); + controls.setAttribute('style', controlsRowStyle); + container.appendChild(controls); + const chartHost = document.createElement('div'); + chartHost.setAttribute('style', 'margin-top:20px;'); + container.appendChild(chartHost); + + let width = 320; + let height = 320; + let innerRadius = 55; + + const renderChart = () => { + const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; + chart.setAttribute('chart-title', 'Donut chart sizing example'); + chart.setAttribute('data', JSON.stringify(data)); + chart.setAttribute('value-inside-donut', '39,000'); + chart.setAttribute('inner-radius', `${innerRadius}`); + chart.width = width; + chart.height = height; + chart.setAttribute('width', `${width}`); + chart.setAttribute('height', `${height}`); + chart.setAttribute('style', `width:${width}px;height:${height}px`); + + chartHost.replaceChildren(chart); + }; + + const widthControl = createSliderField('Width', 'donut-width', width, 200, 640, nextWidth => { + width = nextWidth; + widthControl.setValue(nextWidth); + renderChart(); + }); + controls.appendChild(widthControl.element); + + const heightControl = createSliderField('Height', 'donut-height', height, 200, 640, nextHeight => { + height = nextHeight; + heightControl.setValue(nextHeight); + renderChart(); + }); + controls.appendChild(heightControl.element); + + const innerRadiusControl = createSliderField( + 'Inner radius', + 'donut-inner-radius', + innerRadius, + 1, + 120, + nextRadius => { + innerRadius = nextRadius; + innerRadiusControl.setValue(nextRadius); + renderChart(); + }, + ); + controls.appendChild(innerRadiusControl.element); + + renderChart(); + + return container; +}; + +export const RoundedCorners: Story = () => { + const container = document.createElement('div'); + const controls = document.createElement('div'); + controls.setAttribute('style', controlsRowStyle); + container.appendChild(controls); + + let roundCorners = false; + let hideLabels = false; + + const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; + chart.setAttribute('chart-title', 'Donut chart rounded corners example'); + chart.setAttribute('data', JSON.stringify(data)); + chart.setAttribute('value-inside-donut', '39,000'); + chart.setAttribute('inner-radius', '55'); + chart.setAttribute('style', 'width:320px;height:320px;margin-top:20px;'); + + const renderChart = () => { + chart.hideLabels = hideLabels; + chart.roundCorners = roundCorners; + chart.toggleAttribute('hide-labels', hideLabels); + chart.toggleAttribute('round-corners', roundCorners); + + if (!chart.isConnected) { + container.appendChild(chart); + } + }; + + const roundedCornersControl = createSwitchField( + 'Rounded corners', + 'donut-rounded-corners', + roundCorners, + nextChecked => { + roundCorners = nextChecked; + roundedCornersControl.setValue(nextChecked); + renderChart(); + }, + ); + controls.appendChild(roundedCornersControl.element); + + const hideLabelsControl = createSwitchField('Hide labels', 'donut-rounded-hide-labels', hideLabels, nextChecked => { + hideLabels = nextChecked; + hideLabelsControl.setValue(nextChecked); + renderChart(); + }); + controls.appendChild(hideLabelsControl.element); + + renderChart(); + + return container; +}; + +export const HideLegends: Story = renderComponent(html>` + + +`); + +export const ShowLabelsInPercent: Story = () => { + const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; + chart.setAttribute('chart-title', 'Donut chart percent labels example'); + chart.setAttribute('data', JSON.stringify(data)); + chart.setAttribute('inner-radius', '55'); + chart.toggleAttribute('show-labels-in-percent', true); + chart.hideLabels = false; + + return chart; +}; + +export const RTL: Story = renderComponent(html>` +
+ + +
+`); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts index 789ef91ce7..9e08fd8a11 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts @@ -101,6 +101,10 @@ export const styles = css` display: flex; } + .legend-container[hidden] { + display: none; + } + .legend { display: flex; align-items: center; diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts index 925c1d06bb..d9223dd739 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts @@ -16,35 +16,29 @@ export function donutChartTemplate(): ElementViewTemplate<
- ${when( - x => !x.hideLegends, - html` -
- ${repeat( - x => x.legends, - html` `, - )} -
- `, - )} +
+ ${repeat( + x => x.legends, + html` `, + )} +
${when( x => !x.hideTooltip && x.tooltipProps.isVisible, html` diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.ts index 493696178b..2c430b6ad7 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.ts @@ -2,11 +2,11 @@ import { attr, FASTElement, nullableNumberConverter, observable } from '@microso import { format as d3Format } from 'd3-format'; import { arc as d3Arc, pie as d3Pie, PieArcDatum } from 'd3-shape'; import { + booleanStringConverter, getColorFromToken, getNextColor, getRTL, jsonConverter, - booleanStringConverter, SVG_NAMESPACE_URI, validateChartProps, wrapText, @@ -14,6 +14,36 @@ import { import type { ChartDataPoint, ChartProps, Legend } from './donut-chart.options.js'; export class DonutChart extends FASTElement { + @observable + public tooltipProps = { + isVisible: false, + legend: '', + yValue: '', + color: '', + xPos: 0, + yPos: 0, + }; + + @observable + public legends: Legend[] = []; + + @observable + public activeLegend: string = ''; + protected activeLegendChanged(oldValue: string, newValue: string) { + if (this._isSettingActiveLegend) { + return; + } + + this._updateLegendInteractionState(); + } + + @observable + public isLegendSelected: boolean = false; + + @observable + public selectedLegends: string[] = []; + + @attr({ attribute: 'chart-title' }) public chartTitle?: string; @@ -23,24 +53,21 @@ export class DonutChart extends FASTElement { @attr({ converter: nullableNumberConverter }) public width: number = 200; - @attr({ attribute: 'hide-legends', converter: booleanStringConverter }) + @attr({ attribute: 'hide-legends', mode: 'boolean' }) public hideLegends: boolean = false; - @attr({ attribute: 'hide-tooltip', converter: booleanStringConverter }) + @attr({ attribute: 'hide-tooltip', mode: 'boolean' }) public hideTooltip: boolean = false; - @attr({ attribute: 'hide-labels', converter: booleanStringConverter }) + @attr({ attribute: 'hide-labels', mode: 'boolean' }) public hideLabels: boolean = true; - @attr({ attribute: 'show-labels-in-percent', converter: booleanStringConverter }) + @attr({ attribute: 'show-labels-in-percent', mode: 'boolean' }) public showLabelsInPercent: boolean = false; - @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + @attr({ attribute: 'round-corners', mode: 'boolean' }) public roundCorners: boolean = false; - @attr - public order: 'default' | 'sorted' = 'default'; - @attr({ converter: jsonConverter }) public data!: ChartProps; @@ -54,52 +81,13 @@ export class DonutChart extends FASTElement { public legendListLabel?: string; @attr - public culture?: string; - - @observable - public legends: Legend[] = []; - - @observable - public activeLegend: string = ''; - protected activeLegendChanged(oldValue: string, newValue: string) { - if (newValue === '') { - this._arcs?.forEach(arc => arc.classList.remove('inactive')); - this._arcLabels?.forEach(label => label.classList.remove('inactive')); - } else { - this._arcs?.forEach(arc => { - if (arc.getAttribute('data-id') === newValue) { - arc.classList.remove('inactive'); - } else { - arc.classList.add('inactive'); - } - }); - this._arcLabels?.forEach(label => { - if (label.getAttribute('data-id') === newValue) { - label.classList.remove('inactive'); - } else { - label.classList.add('inactive'); - } - }); - } - - this._updateTextInsideDonut(); - } + public order: 'default' | 'sorted' = 'default'; - @observable - public isLegendSelected: boolean = false; + @attr + public culture?: string; - @observable - public tooltipProps = { - isVisible: false, - legend: '', - yValue: '', - color: '', - xPos: 0, - yPos: 0, - }; - protected tooltipPropsChanged(oldValue: any, newValue: any) { - this._updateTextInsideDonut(); - } + @attr({ attribute: 'allow-multiple-legend-selection', mode: 'boolean' }) + public allowMultipleLegendSelection: boolean = false; public chartContainer!: HTMLDivElement; public group!: SVGGElement; @@ -108,9 +96,13 @@ export class DonutChart extends FASTElement { private _arcs: SVGPathElement[] = []; private _arcLabels: SVGTextElement[] = []; private _isRTL: boolean = false; + private _isSettingActiveLegend: boolean = false; + private _isSettingTooltipProps: boolean = false; private _textInsideDonut?: SVGTextElement; + private _tooltip?: HTMLDivElement; + private readonly _handleMouseLeave = () => { - this.tooltipProps = { isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }; + this._setTooltipProps({ isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }); }; constructor() { @@ -119,110 +111,236 @@ export class DonutChart extends FASTElement { this.elementInternals.role = 'region'; } - public handleLegendMouseoverAndFocus(legendTitle: string) { - if (this.isLegendSelected) { + protected tooltipPropsChanged(oldValue: any, newValue: any) { + if (this._isSettingTooltipProps) { return; } - this.activeLegend = legendTitle; + this._updateTooltipState(); + } + + public handleLegendMouseoverAndFocus(legendTitle: string) { + if (this.allowMultipleLegendSelection) { + if (this.selectedLegends.length > 0) { + return; + } + } else { + if (this.isLegendSelected) { + return; + } + } + + this._setActiveLegend(legendTitle); } public handleLegendMouseoutAndBlur() { - if (this.isLegendSelected) { - return; + if (this.allowMultipleLegendSelection) { + if (this.selectedLegends.length > 0) { + return; + } + } else { + if (this.isLegendSelected) { + return; + } } - this.activeLegend = ''; + this._setActiveLegend(''); } public handleLegendClick(legendTitle: string) { + if (this.allowMultipleLegendSelection) { + const nextSelection = this.selectedLegends.includes(legendTitle) + ? this.selectedLegends.filter(legend => legend !== legendTitle) + : [...this.selectedLegends, legendTitle]; + this.selectedLegends = nextSelection; + if (nextSelection.length === 0) { + this._setActiveLegend(''); + } else if (!nextSelection.includes(this.activeLegend)) { + this._setActiveLegend(nextSelection[nextSelection.length - 1]); + } else { + this._updateLegendInteractionState(); + } + return; + } + if (this.isLegendSelected && this.activeLegend === legendTitle) { - this.activeLegend = ''; + this._setActiveLegend(''); this.isLegendSelected = false; } else { - this.activeLegend = legendTitle; + this._setActiveLegend(legendTitle); this.isLegendSelected = true; } } + public isLegendItemSelected(legendTitle: string) { + return Array.isArray(this.selectedLegends) && this.selectedLegends.includes(legendTitle); + } + + public isLegendItemDimmed(legendTitle: string) { + const highlighted = this._getHighlightedLegends(); + return highlighted.length > 0 && !highlighted.includes(legendTitle); + } + connectedCallback() { this._initializeFromAttributes(); + const initialChartData = this.data ? this._prepareChartData() : undefined; + super.connectedCallback(); + this.addEventListener('mouseleave', this._handleMouseLeave); - if (!this.data) { + if (!this.data || !initialChartData) { return; } - this._initializeAndRender(); + this._isRTL = getRTL(this); + this._render(initialChartData); } - protected dataChanged(_oldValue: ChartProps, newValue: ChartProps) { - if (this.$fastController.isConnected && newValue) { - this._rerender(); + public disconnectedCallback() { + this.removeEventListener('mouseleave', this._handleMouseLeave); + super.disconnectedCallback(); + } + + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { + super.attributeChangedCallback(name, oldValue, newValue); + + if (oldValue === newValue) { + return; + } + + const booleanValue = newValue !== null && newValue !== 'false'; + + if (name === 'round-corners') { + this.roundCorners = booleanValue; + } + if (name === 'hide-labels') { + this.hideLabels = booleanValue; + } + if (name === 'hide-legends') { + this.hideLegends = booleanValue; + } + if (name === 'show-labels-in-percent') { + this.showLabelsInPercent = booleanValue; + } + if (name === 'hide-tooltip') { + this.hideTooltip = booleanValue; + } + if (name === 'allow-multiple-legend-selection') { + this.allowMultipleLegendSelection = booleanValue; } } - protected orderChanged() { - this._rerender(); + protected roundCornersChanged() { + this._scheduleRender(); + } + + protected dataChanged(_oldValue: ChartProps, newValue: ChartProps) { + if (newValue) { + this._scheduleRender(); + } } protected chartTitleChanged() { - this._rerender(); + this._scheduleRender(); + } + + protected widthChanged() { + this._scheduleRender(); } protected heightChanged() { - this._rerender(); + this._scheduleRender(); } - protected widthChanged() { - this._rerender(); + protected innerRadiusChanged() { + this._scheduleRender(); + } + + protected valueInsideDonutChanged() { + this._scheduleRender(); } protected hideLabelsChanged() { - this._rerender(); + this._scheduleRender(); + } + + protected hideLegendsChanged(_oldValue: boolean, newValue: boolean) { + this.shadowRoot?.querySelector('.legend-container')?.toggleAttribute('hidden', newValue); + } + + protected hideTooltipChanged() { + this._updateTooltip(); } protected showLabelsInPercentChanged() { - this._rerender(); + this._scheduleRender(); } protected cultureChanged() { - this._rerender(); + this._scheduleRender(); } - protected roundCornersChanged() { - this._rerender(); + protected orderChanged() { + this._scheduleRender(); + } + + protected allowMultipleLegendSelectionChanged() { + if (!this.allowMultipleLegendSelection) { + this.selectedLegends = []; + this._setActiveLegend(''); + this.isLegendSelected = false; + return; + } + + this._updateLegendInteractionState(); + } + + protected selectedLegendsChanged() { + this._updateLegendInteractionState(); + } + + private _renderPending = false; + + /** + * Schedules a single re-render deferred to the next event-loop task, + * batching all attribute changes from a single Blazor render batch + * (which may span multiple microtask checkpoints due to async JS interop) + * into one render pass. + * Interactive-state changes (activeLegend, tooltipProps) bypass this and + * update immediately. + */ + private _scheduleRender(): void { + if (this._renderPending) { + return; + } + this._renderPending = true; + setTimeout(() => { + this._renderPending = false; + this._rerender(); + }, 0); } private _rerender() { if (!this.$fastController.isConnected || !this.data) { return; } + this._clearChart(); this._initializeAndRender(); } - private _initializeAndRender() { - validateChartProps(this.data, 'data'); - - const chartData = - this.order === 'sorted' ? [...this.data.chartData].sort((a, b) => b.data - a.data) : this.data.chartData; - - chartData.forEach((dataPoint, index) => { - if (dataPoint.color) { - dataPoint.color = getColorFromToken(dataPoint.color); - } else { - dataPoint.color = getNextColor(index); + private _clearChart() { + if (this.group) { + while (this.group.firstChild) { + this.group.removeChild(this.group.firstChild); } - }); - - this.legends = this._getLegends(chartData); - this._isRTL = getRTL(this); - this.elementInternals.ariaLabel = this.chartTitle || `Donut chart with ${chartData.length} segments.`; + } - this._render(chartData); + this._arcs = []; + this._arcLabels = []; + this._textInsideDonut = undefined; } private _initializeFromAttributes() { @@ -240,46 +358,99 @@ export class DonutChart extends FASTElement { } }; - const setNumber = (name: string, assign: (value: number) => void) => { - const value = this.getAttribute(name); - if (value !== null) { - const parsed = nullableNumberConverter.fromView(value); - if (parsed !== null && parsed !== undefined) { - assign(parsed); - } - } - }; + setString('chart-title', value => { + this.chartTitle = value; + }); + setString('height', value => { + this.height = nullableNumberConverter.fromView(value) ?? this.height; + }); + setString('width', value => { + this.width = nullableNumberConverter.fromView(value) ?? this.width; + }); + setString('data', value => { + this.data = jsonConverter.fromView(value) as ChartProps; + }); + setString('inner-radius', value => { + this.innerRadius = nullableNumberConverter.fromView(value) ?? this.innerRadius; + }); + setString('value-inside-donut', value => { + this.valueInsideDonut = value; + }); + setString('legend-list-label', value => { + this.legendListLabel = value; + }); + setString('order', value => { + this.order = value as 'default' | 'sorted'; + }); + setString('culture', value => { + this.culture = value; + }); - setString('data', value => { this.data = jsonConverter.fromView(value) as ChartProps; }); - setString('chart-title', value => { this.chartTitle = value; }); - setString('value-inside-donut', value => { this.valueInsideDonut = value; }); - setString('legend-list-label', value => { this.legendListLabel = value; }); - setString('culture', value => { this.culture = value; }); - setString('order', value => { this.order = value as 'default' | 'sorted'; }); + setBoolean('hide-legends', value => { + this.hideLegends = value; + }); + setBoolean('hide-tooltip', value => { + this.hideTooltip = value; + }); + setBoolean('hide-labels', value => { + this.hideLabels = value; + }); + setBoolean('show-labels-in-percent', value => { + this.showLabelsInPercent = value; + }); + setBoolean('round-corners', value => { + this.roundCorners = value; + }); + setBoolean('allow-multiple-legend-selection', value => { + this.allowMultipleLegendSelection = value; + }); + } + + private _initializeAndRender() { + const chartData = this._prepareChartData(); - setNumber('inner-radius', value => { this.innerRadius = value; }); - setNumber('height', value => { this.height = value; }); - setNumber('width', value => { this.width = value; }); + this._isRTL = getRTL(this); - setBoolean('hide-legends', value => { this.hideLegends = value; }); - setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); - setBoolean('hide-labels', value => { this.hideLabels = value; }); - setBoolean('show-labels-in-percent', value => { this.showLabelsInPercent = value; }); - setBoolean('round-corners', value => { this.roundCorners = value; }); + this._render(chartData); } - private _clearChart() { - while (this.group.firstChild) { - this.group.removeChild(this.group.firstChild); - } - this._arcs = []; - this._arcLabels = []; - this._textInsideDonut = undefined; + private _prepareChartData(): ChartDataPoint[] { + validateChartProps(this.data, 'data'); + + const chartData = this._resolveChartData(); + + this.legends = this._getLegends(chartData); + this.elementInternals.ariaLabel = + this.chartTitle || this.data.chartTitle || `Donut chart with ${chartData.length} segments.`; + + return chartData; + } + + private _resolveChartData(): ChartDataPoint[] { + const sourceData = + this.order === 'sorted' ? [...this.data.chartData].sort((a, b) => b.data - a.data) : this.data.chartData; + const totalValue = sourceData.reduce((sum, point) => sum + (point.data ?? 0), 0); + const minimumValue = totalValue * 0.01; + + return sourceData.map((dataPoint, index) => { + const color = dataPoint.color ? getColorFromToken(dataPoint.color) : getNextColor(index); + const resolvedData = minimumValue > dataPoint.data && dataPoint.data > 0 ? minimumValue : dataPoint.data; + + return { + ...dataPoint, + color, + data: resolvedData, + yAxisCalloutData: + resolvedData !== dataPoint.data + ? dataPoint.yAxisCalloutData ?? dataPoint.data.toLocaleString(this.culture || undefined) + : dataPoint.yAxisCalloutData, + }; + }); } private _render(chartData: ChartDataPoint[]) { - const totalValue = chartData.reduce((total, dataPoint) => total + dataPoint.data, 0); - const outerRadius = (Math.min(this.height, this.width) - 20) / 2; + const totalValue = chartData.reduce((sum, point) => sum + (point.data ?? 0), 0); + const outerRadius = Math.max(0, (Math.min(this.height, this.width) - 20) / 2); const cornerRadius = this.roundCorners ? 3 : 0; const pie = d3Pie() .value(d => d.data) @@ -316,14 +487,14 @@ export class DonutChart extends FASTElement { const bounds = this.getBoundingClientRect(); - this.tooltipProps = { + this._setTooltipProps({ isVisible: true, legend: arcDatum.data.legend, yValue: `${arcDatum.data.data}`, color: arcDatum.data.color!, xPos: this._isRTL ? bounds.right - event.clientX : event.clientX - bounds.left, yPos: event.clientY - bounds.top - 85, - }; + }); }); path.addEventListener('focus', event => { if (this.activeLegend !== '' && this.activeLegend !== arcDatum.data.legend) { @@ -333,7 +504,7 @@ export class DonutChart extends FASTElement { const rootBounds = this.getBoundingClientRect(); const arcBounds = path.getBoundingClientRect(); - this.tooltipProps = { + this._setTooltipProps({ isVisible: true, legend: arcDatum.data.legend, yValue: `${arcDatum.data.data}`, @@ -342,10 +513,10 @@ export class DonutChart extends FASTElement { ? rootBounds.right - arcBounds.left - arcBounds.width / 2 : arcBounds.left + arcBounds.width / 2 - rootBounds.left, yPos: arcBounds.top - rootBounds.top - 85, - }; + }); }); path.addEventListener('blur', event => { - this.tooltipProps = { isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }; + this._setTooltipProps({ isVisible: false, legend: '', yValue: '', color: '', xPos: 0, yPos: 0 }); }); const label = this._createArcLabel(arc, arcDatum, totalValue, outerRadius); @@ -355,6 +526,9 @@ export class DonutChart extends FASTElement { } }); + this._applyActiveLegendState(); + this._applyLegendButtonState(); + if (this.valueInsideDonut) { this._textInsideDonut = document.createElementNS(SVG_NAMESPACE_URI, 'text'); this.group.appendChild(this._textInsideDonut); @@ -365,15 +539,147 @@ export class DonutChart extends FASTElement { this._textInsideDonut.setAttribute('dominant-baseline', 'middle'); this._updateTextInsideDonut(); } + + this._updateTooltip(); } private _getLegends(chartData: ChartDataPoint[]): Legend[] { - return chartData.map((d, index) => ({ - legend: d.legend, + return chartData.map(d => ({ + title: d.legend, color: d.color!, })); } + private _getHighlightedLegends(): string[] { + if (this.allowMultipleLegendSelection) { + if (Array.isArray(this.selectedLegends) && this.selectedLegends.length > 0) { + return this.selectedLegends; + } + return this.activeLegend ? [this.activeLegend] : []; + } + return this.activeLegend ? [this.activeLegend] : []; + } + + private _applyActiveLegendState() { + if (!this._arcs || !this._arcLabels) { + return; + } + + const highlighted = this._getHighlightedLegends(); + + if (highlighted.length === 0) { + this._arcs.forEach(arc => { + arc.classList.remove('inactive'); + arc.setAttribute('tabindex', '0'); + }); + this._arcLabels.forEach(label => label.classList.remove('inactive')); + return; + } + + this._arcs.forEach(arc => { + const legendId = arc.getAttribute('data-id'); + const isActive = legendId !== null && highlighted.includes(legendId); + arc.classList.toggle('inactive', !isActive); + arc.setAttribute('tabindex', isActive ? '0' : '-1'); + }); + this._arcLabels.forEach(label => { + const legendId = label.getAttribute('data-id'); + label.classList.toggle('inactive', legendId === null || !highlighted.includes(legendId)); + }); + } + + private _updateLegendInteractionState() { + this._applyActiveLegendState(); + this._applyLegendButtonState(); + this._updateTextInsideDonut(); + } + + private _setActiveLegend(value: string) { + this._isSettingActiveLegend = true; + this.activeLegend = value; + this._isSettingActiveLegend = false; + this._updateLegendInteractionState(); + } + + private _applyLegendButtonState() { + const legends = this.shadowRoot?.querySelectorAll('.legend'); + if (!legends) { + return; + } + + const highlighted = this._getHighlightedLegends(); + legends.forEach(button => { + const title = button.querySelector('.legend-text')?.textContent ?? ''; + const isActive = highlighted.length === 0 || highlighted.includes(title); + button.classList.toggle('inactive', !isActive); + button.setAttribute('aria-selected', `${highlighted.includes(title)}`); + }); + } + + private _updateTooltip() { + if (!this.shadowRoot) { + return; + } + + if (this.hideTooltip || !this.tooltipProps.isVisible) { + this._tooltip?.remove(); + this._tooltip = undefined; + return; + } + + if (!this._tooltip || !this._tooltip.isConnected) { + this._tooltip = this.shadowRoot.querySelector('.tooltip') ?? document.createElement('div'); + + if (!this._tooltip.classList.contains('tooltip')) { + this._tooltip.classList.add('tooltip'); + } + + if (!this._tooltip.isConnected) { + const body = document.createElement('div'); + body.classList.add('tooltip-body'); + + const legendText = document.createElement('div'); + legendText.classList.add('tooltip-legend-text'); + body.appendChild(legendText); + + const contentY = document.createElement('div'); + contentY.classList.add('tooltip-content-y'); + body.appendChild(contentY); + + this._tooltip.appendChild(body); + this.shadowRoot.appendChild(this._tooltip); + } + } + + this._tooltip.style.insetInlineStart = `${this.tooltipProps.xPos}px`; + this._tooltip.style.top = `${this.tooltipProps.yPos}px`; + + const body = this._tooltip.querySelector('.tooltip-body'); + const legendText = this._tooltip.querySelector('.tooltip-legend-text'); + const contentY = this._tooltip.querySelector('.tooltip-content-y'); + + body?.style.setProperty('border-color', this.tooltipProps.color); + if (legendText) { + legendText.textContent = this.tooltipProps.legend; + } + if (contentY) { + contentY.style.setProperty('color', this.tooltipProps.color); + contentY.textContent = this.tooltipProps.yValue; + } + } + + private _updateTooltipState() { + this._updateTextInsideDonut(); + this._updateTooltip(); + } + + private _setTooltipProps(value: typeof this.tooltipProps) { + this._isSettingTooltipProps = true; + this.tooltipProps = value; + this._isSettingTooltipProps = false; + this._updateTooltipState(); + } + private _createArcLabel( arc: ReturnType>>, arcDatum: PieArcDatum, @@ -416,19 +722,23 @@ export class DonutChart extends FASTElement { private _getTextInsideDonut(valueInsideDonut: string) { let textInsideDonut = valueInsideDonut; - if (valueInsideDonut && (this.activeLegend !== '' || this.tooltipProps.isVisible)) { + const highlighted = this._getHighlightedLegends(); + const singleHighlight = + highlighted.length === 1 + ? highlighted[0] + : this.tooltipProps.isVisible + ? this.tooltipProps.legend + : null; + + if (valueInsideDonut && singleHighlight) { const highlightedDataPoint = this.data.chartData.find( - dataPoint => - dataPoint.legend === this.activeLegend || - (this.tooltipProps.isVisible && dataPoint.legend === this.tooltipProps.legend), + dataPoint => dataPoint.legend === singleHighlight, ); - if (this.showLabelsInPercent) { - const total = this.data.chartData.reduce((acc, point) => acc + point.data, 0); - const percentage = total > 0 ? Math.round(((highlightedDataPoint?.data ?? 0) / total) * 100) : 0; - textInsideDonut = `${percentage}%`; - } else { + if (highlightedDataPoint) { textInsideDonut = - highlightedDataPoint!.calloutData ?? highlightedDataPoint!.data.toLocaleString(this.culture || undefined); + highlightedDataPoint.yAxisCalloutData ?? + highlightedDataPoint.calloutData ?? + highlightedDataPoint.data.toLocaleString(this.culture || undefined); } } diff --git a/src/Charts.Scripts/src/helpers.stories.ts b/src/Charts.Scripts/src/helpers.stories.ts new file mode 100644 index 0000000000..a568c4b82b --- /dev/null +++ b/src/Charts.Scripts/src/helpers.stories.ts @@ -0,0 +1,101 @@ +import type { ElementViewTemplate, FASTElement, ViewTemplate } from '@microsoft/fast-element'; +import type { AnnotatedStoryFn, Args, ComponentAnnotations, Renderer, StoryAnnotations } from 'storybook/internal/csf'; + +/** + * A helper that returns a function to bind a Storybook story to a ViewTemplate. + * + * @param template - The ViewTemplate to render + * @returns - a function to bind a Storybook story + */ +export function renderComponent(template: ViewTemplate): (args: TArgs) => Element | DocumentFragment { + return function (args) { + const storyFragment = new DocumentFragment(); + template.render(args, storyFragment); + if (storyFragment.childElementCount === 1) { + return storyFragment.firstElementChild!; + } + return storyFragment; + }; +} + +export declare interface FASTComponentsRenderer extends Renderer { + canvasElement: FASTElement; + component: typeof FASTElement | string; + storyResult: string | Node | DocumentFragment | ElementViewTemplate; +} + +/** + * A helper that returns a function to bind a Storybook story to a ViewTemplate. + */ +export type FASTFramework = Renderer & { + component: typeof FASTElement; + storyResult: FASTElement | Element | DocumentFragment; +}; + +/** + * Metadata to configure the stories for a component. + */ +export declare type Meta = ComponentAnnotations>; + +/** + * Story object that represents a CSFv3 component example. + * + * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) + */ +export declare type StoryObj = StoryAnnotations>; + +/** + * Story function that represents a CSFv2 component example. + */ +export declare type StoryFn = AnnotatedStoryFn; + +/** + * Story function that represents a CSFv2 component example. + * + * NOTE that in Storybook 7.0, this type will be renamed to `StoryFn` and replaced by the current `StoryObj` type. + */ +export declare type Story = StoryFn>; + +/** + * Combined Storybook story args. + */ +export type StoryArgs = Partial> & Args; + +export function generateImage({ + width, + height = width, + backgroundColor = 'rgb(204, 204, 204)', + color = 'rgb(150, 150, 150)', + text = `${width} x ${height}`, +}: { + width: number; + height?: number; + backgroundColor?: string; + color?: string; + text?: string; +}): string { + const canvas = document.createElement('canvas'); + const context = canvas.getContext('2d') as CanvasRenderingContext2D; + + canvas.width = width; + canvas.height = height; + + // Clear the canvas. + context.clearRect(0, 0, canvas.width, canvas.height); + + // get the font size to fit the text + context.font = '1px sans-serif'; + const maxFontSize = Math.max(width / context.measureText(text).width / 2, 7); + + // Draw the background + context.fillStyle = backgroundColor; + context.fillRect(0, 0, canvas.width, canvas.height); + + context.font = `${maxFontSize}px Helvetica, Arial, sans-serif`; + context.textAlign = 'center'; + context.textBaseline = 'middle'; + context.fillStyle = color; + context.fillText(text, canvas.width / 2, canvas.height / 2); + + return canvas.toDataURL('image/png'); +} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts index 8962554af9..2d5627cc30 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts @@ -4,8 +4,8 @@ import { getNextColor, getRTL, jsonConverter, - booleanStringConverter, - SVG_NAMESPACE_URI + booleanStringConverter, + SVG_NAMESPACE_URI, } from '../utils/chart-helpers.js'; import type { AxisCategoryOrder, @@ -203,10 +203,10 @@ export class HorizontalBarChartWithAxis extends FASTElement { @attr({ attribute: 'enable-gradient', converter: booleanStringConverter }) public enableGradient: boolean = false; - @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + @attr({ attribute: 'round-corners', mode: 'boolean' }) public roundCorners: boolean = false; - @attr({ attribute: 'allow-multiple-legend-selection', converter: booleanStringConverter }) + @attr({ attribute: 'allow-multiple-legend-selection', mode: 'boolean' }) public allowMultipleLegendSelection: boolean = false; @attr({ attribute: 'bar-height' }) @@ -285,6 +285,14 @@ export class HorizontalBarChartWithAxis extends FASTElement { } } + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { + super.attributeChangedCallback(name, oldValue, newValue); + + if (name === 'round-corners' && oldValue !== newValue) { + this.roundCorners = newValue !== null && newValue !== 'false'; + } + } + public disconnectedCallback() { this._resizeObserver?.disconnect(); super.disconnectedCallback(); @@ -610,31 +618,79 @@ export class HorizontalBarChartWithAxis extends FASTElement { } }; - setString('data', value => { this.data = jsonConverter.fromView(value) as HorizontalBarChartWithAxisDataPoint[]; }); - setString('chart-title', value => { this.chartTitle = value; }); - setString('width', value => { this.width = value; }); - setString('legend-list-label', value => { this.legendListLabel = value; }); - setString('bar-height', value => { this.barHeight = value; }); - setString('height', value => { this.height = value; }); - setString('x-axis-tick-count', value => { this.xAxisTickCount = value; }); - setString('y-axis-tick-count', value => { this.yAxisTickCount = value; }); - setString('y-axis-padding', value => { this.yAxisPadding = value; }); - setString('x-min-value', value => { this.xMinValue = value; }); - setString('x-max-value', value => { this.xMaxValue = value; }); - setString('y-min-value', value => { this.yMinValue = value; }); - setString('y-max-value', value => { this.yMaxValue = value; }); - setString('y-axis-category-order', value => { this.yAxisCategoryOrder = value as AxisCategoryOrder; }); - setString('culture', value => { this.culture = value; }); - - setBoolean('hide-legends', value => { this.hideLegends = value; }); - setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); - setBoolean('hide-labels', value => { this.hideLabels = value; }); - setBoolean('show-y-axis-labels', value => { this.showYAxisLabels = value; }); - setBoolean('show-y-axis-labels-tooltip', value => { this.showYAxisLabelsTooltip = value; }); - setBoolean('use-single-color', value => { this.useSingleColor = value; }); - setBoolean('enable-gradient', value => { this.enableGradient = value; }); - setBoolean('round-corners', value => { this.roundCorners = value; }); - setBoolean('allow-multiple-legend-selection', value => { this.allowMultipleLegendSelection = value; }); + setString('data', value => { + this.data = jsonConverter.fromView(value) as HorizontalBarChartWithAxisDataPoint[]; + }); + setString('chart-title', value => { + this.chartTitle = value; + }); + setString('width', value => { + this.width = value; + }); + setString('legend-list-label', value => { + this.legendListLabel = value; + }); + setString('bar-height', value => { + this.barHeight = value; + }); + setString('height', value => { + this.height = value; + }); + setString('x-axis-tick-count', value => { + this.xAxisTickCount = value; + }); + setString('y-axis-tick-count', value => { + this.yAxisTickCount = value; + }); + setString('y-axis-padding', value => { + this.yAxisPadding = value; + }); + setString('x-min-value', value => { + this.xMinValue = value; + }); + setString('x-max-value', value => { + this.xMaxValue = value; + }); + setString('y-min-value', value => { + this.yMinValue = value; + }); + setString('y-max-value', value => { + this.yMaxValue = value; + }); + setString('y-axis-category-order', value => { + this.yAxisCategoryOrder = value as AxisCategoryOrder; + }); + setString('culture', value => { + this.culture = value; + }); + + setBoolean('hide-legends', value => { + this.hideLegends = value; + }); + setBoolean('hide-tooltip', value => { + this.hideTooltip = value; + }); + setBoolean('hide-labels', value => { + this.hideLabels = value; + }); + setBoolean('show-y-axis-labels', value => { + this.showYAxisLabels = value; + }); + setBoolean('show-y-axis-labels-tooltip', value => { + this.showYAxisLabelsTooltip = value; + }); + setBoolean('use-single-color', value => { + this.useSingleColor = value; + }); + setBoolean('enable-gradient', value => { + this.enableGradient = value; + }); + setBoolean('round-corners', value => { + this.roundCorners = value; + }); + setBoolean('allow-multiple-legend-selection', value => { + this.allowMultipleLegendSelection = value; + }); } private _clearChart() { diff --git a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts index 9dea5b158c..c715797bbd 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.ts @@ -5,7 +5,7 @@ import { jsonConverter, booleanStringConverter, SVG_NAMESPACE_URI, - validateChartPropsArray + validateChartPropsArray, } from '../utils/chart-helpers.js'; import type { ChartDataPoint, ChartProps } from './horizontal-bar-chart.options.js'; import { Variant } from './horizontal-bar-chart.options.js'; @@ -34,7 +34,7 @@ export class HorizontalBarChart extends FASTElement { @attr({ attribute: 'hide-labels', converter: booleanStringConverter }) public hideLabels: boolean = false; - @attr({ attribute: 'round-corners', converter: booleanStringConverter }) + @attr({ attribute: 'round-corners', mode: 'boolean' }) public roundCorners: boolean = false; @attr({ attribute: 'chart-data-mode' }) @@ -135,6 +135,14 @@ export class HorizontalBarChart extends FASTElement { this._initializeAll(); } + attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) { + super.attributeChangedCallback(name, oldValue, newValue); + + if (name === 'round-corners' && oldValue !== newValue) { + this.roundCorners = newValue !== null && newValue !== 'false'; + } + } + protected dataChanged(_oldValue: ChartProps[], newValue: ChartProps[]) { if (this.$fastController.isConnected && newValue) { this._clearChart(); @@ -185,19 +193,43 @@ export class HorizontalBarChart extends FASTElement { } }; - setString('data', value => { this.data = jsonConverter.fromView(value) as ChartProps[]; }); - setString('width', value => { this.width = value; }); - setString('height', value => { this.height = value; }); - setString('variant', value => { this.variant = value as Variant; }); - setString('chart-data-mode', value => { this.chartDataMode = value as 'default' | 'fraction' | 'percentage'; }); - setString('legend-list-label', value => { this.legendListLabel = value; }); - setString('chart-title', value => { this.chartTitle = value; }); - - setBoolean('hide-ratio', value => { this.hideRatio = value; }); - setBoolean('hide-labels', value => { this.hideLabels = value; }); - setBoolean('round-corners', value => { this.roundCorners = value; }); - setBoolean('hide-legends', value => { this.hideLegends = value; }); - setBoolean('hide-tooltip', value => { this.hideTooltip = value; }); + setString('data', value => { + this.data = jsonConverter.fromView(value) as ChartProps[]; + }); + setString('width', value => { + this.width = value; + }); + setString('height', value => { + this.height = value; + }); + setString('variant', value => { + this.variant = value as Variant; + }); + setString('chart-data-mode', value => { + this.chartDataMode = value as 'default' | 'fraction' | 'percentage'; + }); + setString('legend-list-label', value => { + this.legendListLabel = value; + }); + setString('chart-title', value => { + this.chartTitle = value; + }); + + setBoolean('hide-ratio', value => { + this.hideRatio = value; + }); + setBoolean('hide-labels', value => { + this.hideLabels = value; + }); + setBoolean('round-corners', value => { + this.roundCorners = value; + }); + setBoolean('hide-legends', value => { + this.hideLegends = value; + }); + setBoolean('hide-tooltip', value => { + this.hideTooltip = value; + }); } private _clearChart() { @@ -437,9 +469,10 @@ export class HorizontalBarChart extends FASTElement { .attr('rx', `${this.roundCorners ? 3 : 0}`) .attr( 'x', - `${_isRTL - ? 100 - startingPoint[index] - value - barSpacingInPercent * index - : startingPoint[index] + barSpacingInPercent * index + `${ + _isRTL + ? 100 - startingPoint[index] - value - barSpacingInPercent * index + : startingPoint[index] + barSpacingInPercent * index }%`, ) .attr('y', 0) @@ -449,7 +482,10 @@ export class HorizontalBarChart extends FASTElement { this._bars.push(rect.node()!); }; - const containerDiv = d3Create('div').attr('style', 'position: relative; margin-bottom: var(--spacingVerticalMNudge);'); + const containerDiv = d3Create('div').attr( + 'style', + 'position: relative; margin-bottom: var(--spacingVerticalMNudge);', + ); const barTitleDiv = containerDiv.append('div').attr('class', 'bar-title-div'); barTitleDiv @@ -504,7 +540,7 @@ export class HorizontalBarChart extends FASTElement { .attr( 'aria-label', data?.chartSeriesTitle ?? - `Series with ${data.chartData.length}${data.chartData.length > 1 ? ' stacked' : ''} bars.`, + `Series with ${data.chartData.length}${data.chartData.length > 1 ? ' stacked' : ''} bars.`, ) .selectAll('g') .data(data.chartData!) @@ -547,9 +583,10 @@ export class HorizontalBarChart extends FASTElement { .attr('class', 'bar-label') .attr( 'x', - `${this._isRTL - ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent - : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent + `${ + this._isRTL + ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent + : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent }%`, ) .attr('textAnchor', 'start') @@ -566,9 +603,10 @@ export class HorizontalBarChart extends FASTElement { .attr('class', 'bar-label') .attr( 'x', - `${this._isRTL - ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent - : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent + `${ + this._isRTL + ? 100 - (startingPoint[startingPoint.length - 1] || 0) - value - totalMarginPercent + : (startingPoint[startingPoint.length - 1] || 0) + value + totalMarginPercent }%`, ) .attr('textAnchor', 'start') diff --git a/src/Charts/ChartJson.cs b/src/Charts/ChartJson.cs index 2a3d4d0d3f..9eb1c882cb 100644 --- a/src/Charts/ChartJson.cs +++ b/src/Charts/ChartJson.cs @@ -30,4 +30,18 @@ public static string Serialize(IReadOnlyList value) => JsonSerializer.Serialize( value, HorizontalBarChartDataJsonSerializerContext.Default.IReadOnlyListHorizontalBarChartSeries); + + /// + /// Serializes horizontal bar chart data with using the horizontal bar chart + /// serializer context. + /// + /// The horizontal bar chart series collection. + /// + /// A JSON string suitable for the fluent-horizontal-bar-chart + /// component. + /// + public static string Serialize(IReadOnlyList value) => + JsonSerializer.Serialize( + value, + HorizontalBarChartWithAxisDataJsonSerializerContext.Default.IReadOnlyListHorizontalBarChartWithAxisDataPoint); } diff --git a/src/Charts/Charts/DonutChart/FluentDonutChart.razor b/src/Charts/Charts/DonutChart/FluentDonutChart.razor index ef7a29066a..cff1a20adb 100644 --- a/src/Charts/Charts/DonutChart/FluentDonutChart.razor +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor @@ -1,18 +1,22 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts @inherits FluentComponentBase - diff --git a/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs index 6268004a49..a74c5c1eac 100644 --- a/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor.cs @@ -74,6 +74,18 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration [Parameter] public bool HideTooltip { get; set; } + /// + /// Gets or sets whether bars/arcs in the chart should have rounded corners. + /// + [Parameter] + public bool RoundedCorners { get; set; } + + /// + /// Gets or sets whether label values should be displayed as percentages of the total rather than raw values. + /// + [Parameter] + public bool ShowLabelsInPercent { get; set; } + /// /// Gets or sets the inner radius of the component, in pixels. /// @@ -94,4 +106,12 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration ///
[Parameter] public string? LegendListLabel { get; set; } + + /// + /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. + /// When , clicking a legend item adds it to the active selection rather than replacing the current selection. + /// When (default), only a single legend item can be selected at a time. + /// + [Parameter] + public bool AllowMultipleLegendSelection { get; set; } } diff --git a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor new file mode 100644 index 0000000000..b84df42236 --- /dev/null +++ b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor @@ -0,0 +1,21 @@ +@namespace Microsoft.FluentUI.AspNetCore.Components.Charts +@inherits FluentComponentBase + + + diff --git a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs new file mode 100644 index 0000000000..782ac38b01 --- /dev/null +++ b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs @@ -0,0 +1,77 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// A FluentHorizontalBarChartWithAxis is a component that displays data in a horizontal bar chart format with an axis. +/// +public partial class FluentHorizontalBarChartWithAxis : FluentComponentBase +{ + /// + public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + /// + internal string? ClassValue => DefaultClassBuilder + .AddClass("fluent-horizontal-bar-chart-with-axis") + .Build(); + + /// + internal string? StyleValue => DefaultStyleBuilder + .Build(); + + /// + /// Gets or sets the data for the horizontal bar chart. + /// + [Parameter, EditorRequired] + public IReadOnlyList ChartData { get; set; } = []; + + /// + /// Gets or sets the title text displayed on the chart. + /// + [Parameter] + public string? ChartTitle { get; set; } + + /// + /// Gets or sets the height of the horizontal bar chart. + /// + [Parameter] + public int? Height { get; set; } + + /// + /// Gets or sets the width of the horizontal bar chart. + /// + [Parameter] + public int? Width { get; set; } + + /// + /// Gets or sets a value indicating whether legends are hidden in the component output. + /// + [Parameter] + public bool HideLegends { get; set; } + + /// + /// Gets or sets a value indicating whether the tooltip is hidden. + /// + [Parameter] + public bool HideTooltip { get; set; } + + /// + /// Gets or sets a value indicating whether the labels are hidden. + /// + [Parameter] + public bool HideLabels { get; set; } + + /// + /// Gets or sets the label displayed for the legend list. + /// + [Parameter] + public string? LegendListLabel { get; set; } +} diff --git a/src/Charts/Enums/HorizontalBarChartWithAxisCategoryOrder.cs b/src/Charts/Enums/HorizontalBarChartWithAxisCategoryOrder.cs new file mode 100644 index 0000000000..b2c3a71aae --- /dev/null +++ b/src/Charts/Enums/HorizontalBarChartWithAxisCategoryOrder.cs @@ -0,0 +1,139 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.ComponentModel; + +namespace Microsoft.FluentUI.AspNetCore.Components.Enums; + +/// +/// Specifies which axis catagory order visual variant the +/// HorizontalBarChartWithAxis uses. +/// +public enum HorizontalBarChartWithAxisCategoryOrder +{ + /// + /// Use the default category order, which is typically the order in which + /// categories appear in the data source. + /// + [Description("default")] + Default, + + /// + /// Use the data order, which is the order of categories as they are + /// provided in the data payload. This may differ from the default order if + /// the component applies any internal sorting or grouping logic. + /// + [Description("data")] + Data, + + /// + /// Use the category ascending order, which sorts categories alphabetically + /// from A to Z (or in ascending order for non-alphabetic labels). + /// + [Description("category ascending")] + CategoryAscending, + + /// + /// Use the category descending order, which sorts categories alphabetically + /// from Z to A (or in descending order for non-alphabetic labels). + /// + [Description("category descending")] + CategoryDescending, + + /// + /// Use the total ascending order, which sorts categories based on the total + /// value of their associated data points in ascending order (from smallest + /// to largest total). + /// + [Description("total ascending")] + TotalAscending, + + /// + /// Use the total descending order, which sorts categories based on the + /// total value of their associated data points in descending order (from + /// largest to smallest total). + /// + [Description("total descending")] + TotalDescending, + + /// + /// Use the minimal ascending order, which sorts categories based on the + /// minimum value of their associated data points in ascending order (from + /// smallest to largest minimum). + /// + [Description("min ascending")] + MinAscending, + + /// + /// Use the minimal descending order, which sorts categories based on the + /// minimum value of their associated data points in descending order (from + /// largest to smallest minimum). + /// + [Description("min descending")] + MinDescending, + + /// + /// Use the maximum ascending order, which sorts categories based on the + /// maximum value of their associated data points in ascending order (from + /// smallest to largest maximum). + /// + [Description("max ascending")] + MaxAscending, + + /// + /// Use the maximum descending order, which sorts categories based on the + /// maximum value of their associated data points in descending order (from + /// largest to smallest maximum). + /// + [Description("max descending")] + MaxDescending, + + /// + /// Use the sum ascending order, which sorts categories based on the sum of + /// their associated data points in ascending order (from smallest to + /// largest sum). + /// + [Description("sum ascending")] + SumAscending, + + /// + /// Use the sum descending order, which sorts categories based on the sum of + /// their associated data points in descending order (from largest to + /// smallest sum). + /// + [Description("sum descending")] + SumDescending, + + /// + /// Use the mean ascending order, which sorts categories based on the mean + /// (average) value of their associated data points in ascending order (from + /// smallest to largest mean). + /// + [Description("mean ascending")] + MeanAscending, + + /// + /// Use the mean descending order, which sorts categories based on the mean + /// (average) value of their associated data points in descending order + /// (from largest to smallest mean). + /// + [Description("mean descending")] + MeanDescending, + + /// + /// Use the median ascending order, which sorts categories based on the + /// median value of their associated data points in ascending order (from + /// smallest to largest median). + /// + [Description("median ascending")] + MedianAscending, + + /// + /// Use the median descending order, which sorts categories based on the + /// median value of their associated data points in descending order (from + /// largest to smallest median). + /// + [Description("median descending")] + MedianDescending, +} diff --git a/src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs b/src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs new file mode 100644 index 0000000000..2742755e5c --- /dev/null +++ b/src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Provides source-generated JSON serialization metadata for horizontal bar chart payloads. +/// +[JsonSerializable(typeof(HorizontalBarChartWithAxisDataPoint))] +[JsonSerializable(typeof(IReadOnlyList))] +internal sealed partial class HorizontalBarChartWithAxisDataJsonSerializerContext : JsonSerializerContext +{ +} diff --git a/src/Charts/Models/HorizontalBarChartWithAxisModels.cs b/src/Charts/Models/HorizontalBarChartWithAxisModels.cs new file mode 100644 index 0000000000..596cb1d646 --- /dev/null +++ b/src/Charts/Models/HorizontalBarChartWithAxisModels.cs @@ -0,0 +1,96 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +#pragma warning disable MA0048 // File name must match type name + +/// +/// Represents a single data point in a horizontal bar chart series. +/// +public sealed record HorizontalBarChartWithAxisDataPoint +{ + /// + /// Gets the numeric value of the bar segment, which determines its length along the x-axis. + /// + [JsonPropertyName("x")] + public double X { get; init; } + + /// + /// Gets the category or label of the bar segment, which determines its position along the y-axis. + /// + [JsonPropertyName("y")] + public string Y { get; init; } = string.Empty; + + /// + /// Gets the legend text shown for the bar segment. + /// + [JsonPropertyName("legend")] + public string Legend { get; init; } = string.Empty; + + /// + /// Gets the solid color used to render the bar segment. + /// If not provided, the component may fall back to its default palette behavior. + /// + [JsonPropertyName("color")] + public string? Color { get; init; } + + /// + /// Gets the optional two-color gradient used to render the bar segment. + /// The array should contain exactly two color values: start and end. + /// + [JsonPropertyName("gradient")] + public string[]? Gradient { get; init; } + + /// + /// Gets or initializes the accessibility data for the callout. + /// + public string callOutAccessibilityData { get; init; } = string.Empty; + + /// + /// Gets optional callout data for the x-axis portion of the tooltip. + /// + [JsonPropertyName("xAxisCalloutData")] + public string? XAxisCalloutData { get; init; } + + /// + /// Gets optional callout data for the y-axis portion of the tooltip. + /// If not provided, the component may fall back to the numeric data value. + /// + [JsonPropertyName("yAxisCalloutData")] + public string? YAxisCalloutData { get; init; } +} + +/// +/// Represents one horizontal bar chart series in the data payload. +/// +public sealed record HorizontalBarChartSeriesWithAxis +{ + /// + /// Gets the optional title shown for the data series. + /// + [JsonPropertyName("chartSeriesTitle")] + public string? ChartSeriesTitle { get; init; } + + /// + /// Gets the collection of data points rendered within the series. + /// + [JsonPropertyName("chartData")] + public IReadOnlyList ChartData { get; init; } = []; + + /// + /// Gets the optional benchmark value used to render the benchmark indicator. + /// + [JsonPropertyName("benchmarkData")] + public double? BenchmarkData { get; init; } + + /// + /// Gets optional text displayed alongside the chart data for the series. + /// + [JsonPropertyName("chartDataText")] + public string? ChartDataText { get; init; } +} +#pragma warning restore MA0048 // File name must match type name From 561259de74807492545facfe73a76173c4cd4dab Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 5 May 2026 12:58:34 +0200 Subject: [PATCH 09/60] - Update charts ts code - Organize examples --- .../Charts/DebugPages/DonutChartDebug.razor | 35 +- .../HorizontalBarChartWithAxisDebug.razor | 94 +++ .../{ => DonutChart}/DonutChartDefault.razor | 0 .../DonutChartDefaultRTL.razor | 2 +- .../DonutChartHideLabels.razor} | 9 +- .../DonutChartHideLegends.razor | 0 .../DonutChart/DonutChartLabels.razor | 0 .../DonutChartRoundedCorners.razor | 16 +- .../DonutChartShowLabelsInPercent.razor | 7 +- .../{ => DonutChart}/DonutChartSizing.razor | 0 .../HorizontalBarChartBenchmark.razor | 0 .../HorizontalBarChartDefault.razor | 0 .../HorizontalBarChartDefaultRTL.razor | 0 .../HorizontalBarChartSingleBar.razor | 0 ...HorizontalBarChartSingleBarNMVariant.razor | 0 .../HorizontalBarChartSingleDataPoint.razor | 0 .../HorizontalBarChartWithAxisDefault.razor | 0 .../Charts/Pages/FluentDonutChartPage.md | 10 +- .../src/donut-chart/donut-chart.spec.ts | 636 ------------------ .../src/donut-chart/donut-chart.stories.ts | 320 --------- .../src/donut-chart/donut-chart.styles.ts | 9 +- .../src/donut-chart/donut-chart.ts | 129 ++-- src/Charts.Scripts/src/helpers.stories.ts | 101 --- .../horizontal-bar-chart-with-axis.bench.ts | 12 - .../horizontal-bar-chart-with-axis.styles.ts | 11 +- ...horizontal-bar-chart-with-axis.template.ts | 6 +- .../horizontal-bar-chart-with-axis.ts | 373 +++++----- .../horizontal-bar-chart.bench.ts | 12 - .../horizontal-bar-chart.styles.ts | 9 +- .../horizontal-bar-chart.template.ts | 8 +- .../horizontal-bar-chart.ts | 395 ++++++++--- src/Charts.Scripts/src/utils/chart-helpers.ts | 12 + .../Charts/DonutChart/FluentDonutChart.razor | 13 +- .../DonutChart/FluentDonutChart.razor.cs | 15 +- .../FluentHorizontalBarChart.razor | 6 +- .../FluentHorizontalBarChartWithAxis.razor | 12 +- .../FluentHorizontalBarChartWithAxis.razor.cs | 26 + 37 files changed, 770 insertions(+), 1508 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/HorizontalBarChartWithAxisDebug.razor rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartDefault.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartDefaultRTL.razor (91%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{DonutChartLabels.razor => DonutChart/DonutChartHideLabels.razor} (71%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartHideLegends.razor (100%) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLabels.razor rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartRoundedCorners.razor (50%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartShowLabelsInPercent.razor (70%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => DonutChart}/DonutChartSizing.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartBenchmark.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartDefault.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartDefaultRTL.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartSingleBar.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartSingleBarNMVariant.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChart}/HorizontalBarChartSingleDataPoint.razor (100%) rename examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/{ => HorizontalBarChartWithAxis}/HorizontalBarChartWithAxisDefault.razor (100%) delete mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts delete mode 100644 src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts delete mode 100644 src/Charts.Scripts/src/helpers.stories.ts delete mode 100644 src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts delete mode 100644 src/Charts.Scripts/src/horizontal-bar-chart/horizontal-bar-chart.bench.ts diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor index 44998f9323..337fad15fc 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/DonutChartDebug.razor @@ -4,23 +4,34 @@
+ - - + +
+
+ +
@@ -35,9 +46,27 @@ ] }; + private readonly DonutChartData data2 = new() + { + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + new DonutChartDataPoint { Legend = "third", Data = 15000 }, + new DonutChartDataPoint { Legend = "fourth", Data = 1000 }, + new DonutChartDataPoint { Legend = "fifth", Data = 500 }, + new DonutChartDataPoint { Legend = "sixth", Data = 200 }, + new DonutChartDataPoint { Legend = "seventh", Data = 100 }, + new DonutChartDataPoint { Legend = "eighth", Data = 50 }, + new DonutChartDataPoint { Legend = "ninth", Data = 20 }, + new DonutChartDataPoint { Legend = "tenth", Data = 10 }, + ] + }; + private bool roundCorners; private bool hideLabels = true; private bool hideLegends; + private bool hideTooltips; private bool showLabelsInPercent; private bool allowMultipleLegendSelection; } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/HorizontalBarChartWithAxisDebug.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/HorizontalBarChartWithAxisDebug.razor new file mode 100644 index 0000000000..cae42abfa3 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/DebugPages/HorizontalBarChartWithAxisDebug.razor @@ -0,0 +1,94 @@ +@page "/Charts/HorizontalBarChartWithAxis/Debug" +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + + + + @width + + + + + + @height + + + + + +
+ +
+ +
+ +@code { + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { + X = 10000, + Y = "5000", + Legend = "Oranges", + Color = "#637cef", + YAxisCalloutData = "5K", + XAxisCalloutData = "10K", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 20000, + Y = "50000", + Legend = "Dogs", + Color = "#e3008c", + YAxisCalloutData = "50K", + XAxisCalloutData = "20K", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 25000, + Y = "30000", + Legend = "Apples", + Color = "#2aa0a4", + YAxisCalloutData = "30K", + XAxisCalloutData = "25K", + }, + new HorizontalBarChartWithAxisDataPoint { + X = 40000, + Y = "13000", + Legend = "Bananas", + Color = "#9373c0", + YAxisCalloutData = "13K", + XAxisCalloutData = "40K", + }, + ]; + + private int width = 650; + private int height = 350; + private bool singleColor = false; + private bool gradient = false; + private bool roundCorners = false; + private bool allowMultipleLegendSelection = false; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartDefault.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefault.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartDefault.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartDefaultRTL.razor similarity index 91% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartDefaultRTL.razor index e1dc6e894f..2e90b2a1ba 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartDefaultRTL.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartDefaultRTL.razor @@ -3,7 +3,7 @@
diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideLabels.razor similarity index 71% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideLabels.razor index 6ddef9707e..d1ebc809ea 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartLabels.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideLabels.razor @@ -1,10 +1,8 @@ @using Microsoft.FluentUI.AspNetCore.Components.Charts @@ -18,7 +16,8 @@ ChartData = new List { new DonutChartDataPoint { Legend = "first", Data = 20000 }, - new DonutChartDataPoint { Legend = "second", Data = 39000 } + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + new DonutChartDataPoint { Legend = "third", Data = 15000 }, } }; } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideLegends.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartHideLegends.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideLegends.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLabels.razor new file mode 100644 index 0000000000..e69de29bb2 diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartRoundedCorners.razor similarity index 50% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartRoundedCorners.razor index 29e1e3be1f..1e7ef13f7d 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartRoundedCorners.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartRoundedCorners.razor @@ -1,17 +1,11 @@ @using Microsoft.FluentUI.AspNetCore.Components.Charts -
- Rounded corners - Hide labels -
-
+ RoundedCorners="true" />
@code { @@ -19,11 +13,9 @@ { ChartData = [ - new DonutChartDataPoint { Legend = "first", Data = 20000 }, - new DonutChartDataPoint { Legend = "second", Data = 39000 } + new DonutChartDataPoint { Legend = "first", Data = 15000 }, + new DonutChartDataPoint { Legend = "second", Data = 25000 }, + new DonutChartDataPoint { Legend = "third", Data = 35000 }, ] }; - - private bool roundCorners; - private bool hideLabels; } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartShowLabelsInPercent.razor similarity index 70% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartShowLabelsInPercent.razor index 21c2f57da7..b100f385ef 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartShowLabelsInPercent.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartShowLabelsInPercent.razor @@ -2,17 +2,18 @@ + ShowLabelsInPercent="@showLabelsInPercent" /> @code { + private bool showLabelsInPercent = true; private readonly DonutChartData data = new() { ChartData = [ new DonutChartDataPoint { Legend = "first", Data = 20000 }, - new DonutChartDataPoint { Legend = "second", Data = 39000 } + new DonutChartDataPoint { Legend = "second", Data = 40000 }, + new DonutChartDataPoint { Legend = "third", Data = 20000 }, ] }; } diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChartSizing.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartBenchmark.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartBenchmark.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartBenchmark.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartDefault.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefault.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartDefault.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartDefaultRTL.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartDefaultRTL.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartDefaultRTL.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleBar.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBar.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleBar.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleBarNMVariant.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleBarNMVariant.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleBarNMVariant.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleDataPoint.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartSingleDataPoint.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartSingleDataPoint.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor similarity index 100% rename from examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxisDefault.razor rename to examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md index 0ec4e494a5..8ba47ff28e 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -42,14 +42,16 @@ Donut charts are used to show proportion, which expresses a partial value in com {{ DonutChartDefault }} -### With labels - -{{ DonutChartLabels }} - ### With labels as percentages +> [!NOTE] There is no value shown in the center of the Donut Chart because the 'ValueInsideDonut' property is not set. + {{ DonutChartShowLabelsInPercent }} +### Hide labels + +{{ DonutChartHideLabels }} + ### Without legends {{ DonutChartHideLegends }} diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts deleted file mode 100644 index 8306a4a247..0000000000 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.spec.ts +++ /dev/null @@ -1,636 +0,0 @@ -import { test } from '@playwright/test'; -import { expect, fixtureURL } from '../helpers.tests.js'; -import type { DonutChart as FluentDonutChart } from './donut-chart.js'; -import type { ChartDataPoint, ChartProps } from './donut-chart.options.js'; - -const basicTitle = 'Donut chart basic example'; - -const points: ChartDataPoint[] = [ - { - legend: 'first', - data: 20000, - }, - { - legend: 'second', - data: 39000, - }, -]; - -const data: ChartProps = { - chartData: points, -}; - -test.describe('Donut-chart - Basic', () => { - test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - }); - - test('Should render chart properly', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const legends = element.locator('.legend-text'); - await expect(legends.nth(0).getByText('first')).toBeVisible(); - await expect(legends.nth(1).getByText('second')).toBeVisible(); - await expect(element.getByText('39,000')).toBeVisible(); - await expect(element.locator('.arc-label')).toHaveCount(0); - }); - - test('Should render path with proper attributes and css', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const arcList = element.locator('.arc'); - await expect(arcList).toHaveCount(2); - await expect(arcList.nth(0)).toHaveAttribute('fill', '#637cef'); - await expect(arcList.nth(0)).toHaveAttribute('aria-label', 'first, 20000.'); - await expect(arcList.nth(0)).toHaveAttribute( - 'd', - 'M-76.547,47.334A90,90,0,0,1,-1.055,-89.994L-1.055,-54.99A55,55,0,0,0,-46.993,28.577Z', - ); - await expect(arcList.nth(0)).toHaveCSS('fill', 'rgb(99, 124, 239)'); - await expect(arcList.nth(0)).toHaveCSS('--borderRadiusMedium', '4px'); - - await expect(arcList.nth(1)).toHaveAttribute('fill', '#e3008c'); - await expect(arcList.nth(1)).toHaveAttribute('aria-label', 'second, 39000.'); - await expect(arcList.nth(1)).toHaveAttribute( - 'd', - 'M1.055,-89.994A90,90,0,1,1,-75.417,49.115L-45.863,30.358A55,55,0,1,0,1.055,-54.99Z', - ); - await expect(arcList.nth(1)).toHaveCSS('fill', 'rgb(227, 0, 140)'); - await expect(arcList.nth(1)).toHaveCSS('--borderRadiusMedium', '4px'); - }); - - test('Should render legends data properly', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const legends = element.getByRole('option'); - await expect(legends).toHaveCount(2); - const firstLegend = element.getByRole('option', { name: 'First' }); - const secondLegend = element.getByRole('option', { name: 'Second' }); - await expect(firstLegend).toBeVisible(); - await expect(firstLegend).toHaveText('first'); - await expect(firstLegend).toHaveCSS('--borderRadiusMedium', '4px'); - await expect(secondLegend).toBeVisible(); - await expect(secondLegend).toHaveText('second'); - await expect(secondLegend).toHaveCSS('--borderRadiusMedium', '4px'); - }); - - test('Should update path css values with mouse click event on legend', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const firstLegend = element.getByRole('option', { name: 'First' }); - //mouse events - await firstLegend.click(); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '0.1'); - await firstLegend.dispatchEvent('mouseout'); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '0.1'); - await firstLegend.click(); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - }); - - test('Should remove inactive arcs from the tab order when a legend is active', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const firstLegend = element.getByRole('option', { name: 'First' }); - - await expect(firstPath).toHaveAttribute('tabindex', '0'); - await expect(secondPath).toHaveAttribute('tabindex', '0'); - - await firstLegend.dispatchEvent('mouseover'); - - await expect(firstPath).toHaveAttribute('tabindex', '0'); - await expect(secondPath).toHaveAttribute('tabindex', '-1'); - - await firstLegend.dispatchEvent('mouseout'); - - await expect(firstPath).toHaveAttribute('tabindex', '0'); - await expect(secondPath).toHaveAttribute('tabindex', '0'); - }); - - test('Should update path css values with mouse hover event on legend', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const firstLegend = element.getByRole('option', { name: 'First' }); - //mouse events - await firstLegend.dispatchEvent('mouseover'); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '0.1'); - await firstLegend.dispatchEvent('mouseout'); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - }); - - test('Should show callout with mouse hover event on path', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const calloutRoot = element.locator('.tooltip'); - await expect(calloutRoot).toHaveCount(0); - await firstPath.dispatchEvent('mouseover'); - await expect(calloutRoot).toHaveCount(1); - await expect(calloutRoot).toHaveCSS('opacity', '1'); - const calloutLegendText = element.locator('.tooltip-legend-text'); - await expect(calloutLegendText).toHaveText('first'); - const calloutContentY = element.locator('.tooltip-content-y'); - await expect(calloutContentY).toHaveText('20000'); - await firstPath.dispatchEvent('mouseout'); - await expect(calloutRoot).not.toHaveCSS('opacity', '0'); - }); - - test('Should update callout data when mouse moved from one path to another path', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const calloutRoot = element.locator('.tooltip'); - await expect(calloutRoot).toHaveCount(0); - await firstPath.dispatchEvent('mouseover'); - await expect(calloutRoot).toHaveCSS('opacity', '1'); - const calloutLegendText = element.locator('.tooltip-legend-text'); - await expect(calloutLegendText).toHaveText('first'); - const calloutContentY = element.locator('.tooltip-content-y'); - await expect(calloutContentY).toHaveText('20000'); - const secondPath = element.getByLabel('second,'); - await secondPath.dispatchEvent('mouseover'); - await expect(calloutRoot).toHaveCSS('opacity', '1'); - await expect(calloutLegendText).toHaveText('second'); - await expect(calloutContentY).toHaveText('39000'); - }); -}); - -test.describe('Donut-chart - Reactive rerender', () => { - test('Should rerender when data attribute changes after initial render', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await expect(element.locator('.arc')).toHaveCount(2); - - const newData: ChartProps = { - chartData: [ - { legend: 'alpha', data: 10000 }, - { legend: 'beta', data: 20000 }, - { legend: 'gamma', data: 30000 }, - ], - }; - - await element.evaluate((el, d) => { - el.setAttribute('chart-title', 'Updated chart'); - el.setAttribute('data', JSON.stringify(d)); - }, newData); - - await expect(element.locator('.arc')).toHaveCount(3); - await expect(element.locator('.legend-text').nth(0).getByText('alpha')).toBeVisible(); - await expect(element.locator('.legend-text').nth(1).getByText('beta')).toBeVisible(); - await expect(element.locator('.legend-text').nth(2).getByText('gamma')).toBeVisible(); - }); -}); - -test.describe('Donut-chart - hide-labels', () => { - test('Should keep center text visible and hide outside labels when hide-labels is set', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await expect(element.locator('.text-inside-donut')).toHaveCount(1); - await expect(element.locator('.arc-label')).toHaveCount(0); - await expect(element.locator('.arc')).toHaveCount(2); - }); - - test('Should show outside labels when hide-labels is false', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const firstArc = element.locator('.arc').first(); - const defaultPath = await firstArc.getAttribute('d'); - - await element.evaluate(el => { - (el as FluentDonutChart).hideLabels = false; - }); - - await expect(element.locator('.text-inside-donut')).toHaveCount(1); - await expect(element.locator('.arc-label')).toHaveCount(2); - await expect(firstArc).toHaveAttribute('d', defaultPath ?? ''); - }); - - test('Should react to hide-labels string attribute updates', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await expect(element.locator('.arc-label')).toHaveCount(2); - - await element.evaluate(el => { - el.setAttribute('hide-labels', 'true'); - }); - - await expect(element.locator('.arc-label')).toHaveCount(0); - }); -}); - -test.describe('Donut-chart - outside labels', () => { - test('Should render outside labels for visible segments', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await element.evaluate(el => { - (el as FluentDonutChart).hideLabels = false; - }); - const labels = element.locator('.arc-label'); - - await expect(labels).toHaveCount(2); - await expect(labels.nth(0)).toContainText('20'); - await expect(labels.nth(1)).toContainText('39'); - }); - - test('Should render percent outside labels when show-labels-in-percent is set', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await element.evaluate(el => { - (el as FluentDonutChart).hideLabels = false; - }); - const labels = element.locator('.arc-label'); - await expect(labels).toHaveCount(2); - await expect(labels.nth(0)).toContainText('%'); - await expect(labels.nth(1)).toContainText('%'); - }); - - test('Should react to show-labels-in-percent string attribute updates', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const labels = element.locator('.arc-label'); - await expect(labels).toHaveCount(2); - await expect(labels.nth(0)).not.toContainText('%'); - - await element.evaluate(el => { - el.setAttribute('show-labels-in-percent', 'true'); - }); - - await expect(labels.nth(0)).toContainText('%'); - await expect(labels.nth(1)).toContainText('%'); - }); -}); - -test.describe('Donut-chart - hide-tooltip', () => { - test('Should react to hide-tooltip string attribute updates', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - - await firstPath.dispatchEvent('mouseover'); - await expect(element.locator('.tooltip')).toHaveCount(1); - - await element.evaluate(el => { - el.setAttribute('hide-tooltip', 'true'); - }); - - await expect(element.locator('.tooltip')).toHaveCount(0); - }); -}); - -test.describe('Donut-chart - hide-legends', () => { - test('Should react to hide-legends string attribute updates', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const legendContainer = element.locator('.legend-container'); - await expect(legendContainer).toHaveCount(1); - await expect(legendContainer).not.toBeHidden(); - - await element.evaluate(el => { - el.setAttribute('hide-legends', 'true'); - }); - - await expect(legendContainer).toBeHidden(); - }); -}); - -test.describe('Donut-chart - allow-multiple-legend-selection', () => { - const multiData: ChartProps = { - chartData: [ - { legend: 'first', data: 20000 }, - { legend: 'second', data: 39000 }, - { legend: 'third', data: 15000 }, - ], - }; - - test.beforeEach(async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - }); - - test('Should highlight multiple arcs when multiple legends are selected', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const thirdPath = element.getByLabel('third,'); - const firstLegend = element.getByRole('option', { name: 'first' }); - const secondLegend = element.getByRole('option', { name: 'second' }); - - await firstLegend.click(); - await secondLegend.click(); - - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - await expect(thirdPath).toHaveCSS('opacity', '0.1'); - }); - - test('Should deselect a legend on second click in multi-select mode', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const thirdPath = element.getByLabel('third,'); - const firstLegend = element.getByRole('option', { name: 'first' }); - const secondLegend = element.getByRole('option', { name: 'second' }); - - await firstLegend.click(); - await secondLegend.click(); - await firstLegend.click(); // deselect first - - await expect(firstPath).toHaveCSS('opacity', '0.1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - await expect(thirdPath).toHaveCSS('opacity', '0.1'); - }); - - test('Should restore all arcs when all selections are cleared', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const thirdPath = element.getByLabel('third,'); - const firstLegend = element.getByRole('option', { name: 'first' }); - - await firstLegend.click(); - await firstLegend.click(); // deselect — all clear - - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - await expect(thirdPath).toHaveCSS('opacity', '1'); - }); - - test('Should set aria-selected on selected legends', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstLegend = element.getByRole('option', { name: 'first' }); - const secondLegend = element.getByRole('option', { name: 'second' }); - const thirdLegend = element.getByRole('option', { name: 'third' }); - - await firstLegend.click(); - await secondLegend.click(); - - await expect(firstLegend).toHaveAttribute('aria-selected', 'true'); - await expect(secondLegend).toHaveAttribute('aria-selected', 'true'); - await expect(thirdLegend).toHaveAttribute('aria-selected', 'false'); - }); - - test('Should fall back to single-select when allow-multiple-legend-selection is removed', async ({ page }) => { - const element = page.locator('fluent-donut-chart'); - const firstPath = element.getByLabel('first,'); - const secondPath = element.getByLabel('second,'); - const firstLegend = element.getByRole('option', { name: 'first' }); - const secondLegend = element.getByRole('option', { name: 'second' }); - - await firstLegend.click(); - await secondLegend.click(); - - // disable multi-select → selectedLegends should be cleared - await element.evaluate(el => { - el.removeAttribute('allow-multiple-legend-selection'); - }); - - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '1'); - - // now single-select should work - await firstLegend.click(); - await expect(firstPath).toHaveCSS('opacity', '1'); - await expect(secondPath).toHaveCSS('opacity', '0.1'); - }); -}); - -test.describe('Donut-chart - round-corners', () => { - test('Should change arc geometry when round-corners is enabled', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const firstArc = element.locator('.arc').first(); - const defaultPath = await firstArc.getAttribute('d'); - - await element.evaluate(el => { - el.setAttribute('round-corners', 'true'); - }); - - await expect(firstArc).not.toHaveAttribute('d', defaultPath ?? ''); - }); -}); - -test.describe('Donut-chart - order', () => { - const unorderedData: ChartProps = { - chartData: [ - { legend: 'small', data: 5000 }, - { legend: 'large', data: 39000 }, - { legend: 'medium', data: 15000 }, - ], - }; - - test('Should render legends in default order when order is not set', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const legends = element.locator('.legend-text'); - await expect(legends.nth(0).getByText('small')).toBeVisible(); - await expect(legends.nth(1).getByText('large')).toBeVisible(); - await expect(legends.nth(2).getByText('medium')).toBeVisible(); - }); - - test('Should render legends in sorted (descending) order when order="sorted"', async ({ page }) => { - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - const legends = element.locator('.legend-text'); - // Sorted descending: large (39000), medium (15000), small (5000) - await expect(legends.nth(0).getByText('large')).toBeVisible(); - await expect(legends.nth(1).getByText('medium')).toBeVisible(); - await expect(legends.nth(2).getByText('small')).toBeVisible(); - }); - - test('uses chart-title attr and calloutData for highlighted center text', async ({ page }) => { - const calloutData: ChartProps = { - chartData: [ - { legend: 'first', data: 20000, calloutData: '20K highlighted' }, - { legend: 'second', data: 39000 }, - ], - }; - - await page.goto(fixtureURL('components-donutchart--basic')); - await page.setContent(/* html */ ` -
- - -
- `); - await page.waitForFunction(() => customElements.whenDefined('fluent-donut-chart')); - - const element = page.locator('fluent-donut-chart'); - await expect(element.getByText('Callout contract')).toBeVisible(); - - const firstPath = element.getByLabel('first,'); - await firstPath.dispatchEvent('mouseover'); - await expect(element.locator('.text-inside-donut')).toContainText('20K highlighted'); - }); -}); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts deleted file mode 100644 index 8e63f098b8..0000000000 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.stories.ts +++ /dev/null @@ -1,320 +0,0 @@ -import { html } from '@microsoft/fast-element'; -import { - FieldDefinition, - FluentDesignSystem, - LabelDefinition, - SliderDefinition, - SwitchDefinition, -} from '@fluentui/web-components'; -import type { Meta, Story, StoryArgs } from '../helpers.stories.js'; -import { renderComponent } from '../helpers.stories.js'; -import { DonutChart as FluentDonutChart } from './donut-chart.js'; -import type { ChartDataPoint, ChartProps } from './donut-chart.options.js'; - -type FluentSliderElement = HTMLElement & { value: string }; -type FluentSwitchElement = HTMLElement & { checked: boolean }; - -const ensureDefinition = (tagName: string, define: () => void) => { - if (!customElements.get(tagName)) { - define(); - } -}; - -ensureDefinition('fluent-field', () => FieldDefinition.define(FluentDesignSystem.registry)); -ensureDefinition('fluent-label', () => LabelDefinition.define(FluentDesignSystem.registry)); -ensureDefinition('fluent-slider', () => SliderDefinition.define(FluentDesignSystem.registry)); -ensureDefinition('fluent-switch', () => SwitchDefinition.define(FluentDesignSystem.registry)); - -const controlsRowStyle = 'display:flex;flex-wrap:wrap;gap:16px 24px;align-items:end;'; -const sliderFieldStyle = 'min-width:220px;flex:1 1 220px;'; -const toggleFieldStyle = 'min-width:220px;'; - -const createSliderField = ( - labelText: string, - id: string, - value: number, - min: number, - max: number, - onChange: (nextValue: number) => void, -) => { - const field = document.createElement('fluent-field'); - field.setAttribute('label-position', 'above'); - field.setAttribute('style', sliderFieldStyle); - - const label = document.createElement('label'); - label.slot = 'label'; - label.htmlFor = id; - label.textContent = labelText; - field.appendChild(label); - - const slider = document.createElement('fluent-slider') as FluentSliderElement; - slider.slot = 'input'; - slider.id = id; - slider.setAttribute('min', `${min}`); - slider.setAttribute('max', `${max}`); - slider.value = `${value}`; - slider.setAttribute('value', `${value}`); - field.appendChild(slider); - - const message = document.createElement('fluent-label'); - message.slot = 'message'; - message.textContent = `${value}`; - field.appendChild(message); - - slider.addEventListener('change', () => onChange(Number(slider.value))); - - return { - element: field, - setValue: (nextValue: number) => { - slider.value = `${nextValue}`; - slider.setAttribute('value', `${nextValue}`); - message.textContent = `${nextValue}`; - }, - }; -}; - -const createSwitchField = ( - labelText: string, - id: string, - checked: boolean, - onChange: (nextChecked: boolean) => void, -) => { - const field = document.createElement('fluent-field'); - field.setAttribute('label-position', 'after'); - field.setAttribute('style', toggleFieldStyle); - - const label = document.createElement('label'); - label.slot = 'label'; - label.htmlFor = id; - label.textContent = labelText; - field.appendChild(label); - - const control = document.createElement('fluent-switch') as FluentSwitchElement; - control.slot = 'input'; - control.id = id; - control.checked = checked; - control.toggleAttribute('checked', checked); - control.addEventListener('change', () => onChange(control.checked)); - field.appendChild(control); - - return { - element: field, - setValue: (nextChecked: boolean) => { - control.checked = nextChecked; - control.toggleAttribute('checked', nextChecked); - }, - }; -}; - -const basicTitle = 'Donut chart basic example'; -const sortedTitle = 'Sorted donut chart example'; - -const points: ChartDataPoint[] = [ - { - legend: 'first', - data: 20000, - }, - { - legend: 'second', - data: 39000, - }, -]; - -const data: ChartProps = { - chartData: points, -}; - -const sortedPoints: ChartDataPoint[] = [ - { - legend: 'small', - data: 5000, - }, - { - legend: 'large', - data: 39000, - }, - { - legend: 'medium', - data: 15000, - }, -]; - -const sortedData: ChartProps = { - chartData: sortedPoints, -}; - -const storyTemplate = html>` - - -`; - -export default { - title: 'Components/DonutChart', -} as Meta; - -export const Basic: Story = renderComponent(storyTemplate).bind({}); - -export const OutsideLabels: Story = () => { - const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; - chart.setAttribute('chart-title', 'Donut chart outside labels example'); - chart.setAttribute('data', JSON.stringify(data)); - chart.setAttribute('value-inside-donut', '39,000'); - chart.setAttribute('inner-radius', '85'); - chart.setAttribute('width', '320'); - chart.setAttribute('height', '320'); - chart.setAttribute('style', 'width:320px;height:320px'); - chart.hideLabels = false; - - return chart; -}; - -export const Sizing: Story = () => { - const container = document.createElement('div'); - const controls = document.createElement('div'); - controls.setAttribute('style', controlsRowStyle); - container.appendChild(controls); - const chartHost = document.createElement('div'); - chartHost.setAttribute('style', 'margin-top:20px;'); - container.appendChild(chartHost); - - let width = 320; - let height = 320; - let innerRadius = 55; - - const renderChart = () => { - const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; - chart.setAttribute('chart-title', 'Donut chart sizing example'); - chart.setAttribute('data', JSON.stringify(data)); - chart.setAttribute('value-inside-donut', '39,000'); - chart.setAttribute('inner-radius', `${innerRadius}`); - chart.width = width; - chart.height = height; - chart.setAttribute('width', `${width}`); - chart.setAttribute('height', `${height}`); - chart.setAttribute('style', `width:${width}px;height:${height}px`); - - chartHost.replaceChildren(chart); - }; - - const widthControl = createSliderField('Width', 'donut-width', width, 200, 640, nextWidth => { - width = nextWidth; - widthControl.setValue(nextWidth); - renderChart(); - }); - controls.appendChild(widthControl.element); - - const heightControl = createSliderField('Height', 'donut-height', height, 200, 640, nextHeight => { - height = nextHeight; - heightControl.setValue(nextHeight); - renderChart(); - }); - controls.appendChild(heightControl.element); - - const innerRadiusControl = createSliderField( - 'Inner radius', - 'donut-inner-radius', - innerRadius, - 1, - 120, - nextRadius => { - innerRadius = nextRadius; - innerRadiusControl.setValue(nextRadius); - renderChart(); - }, - ); - controls.appendChild(innerRadiusControl.element); - - renderChart(); - - return container; -}; - -export const RoundedCorners: Story = () => { - const container = document.createElement('div'); - const controls = document.createElement('div'); - controls.setAttribute('style', controlsRowStyle); - container.appendChild(controls); - - let roundCorners = false; - let hideLabels = false; - - const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; - chart.setAttribute('chart-title', 'Donut chart rounded corners example'); - chart.setAttribute('data', JSON.stringify(data)); - chart.setAttribute('value-inside-donut', '39,000'); - chart.setAttribute('inner-radius', '55'); - chart.setAttribute('style', 'width:320px;height:320px;margin-top:20px;'); - - const renderChart = () => { - chart.hideLabels = hideLabels; - chart.roundCorners = roundCorners; - chart.toggleAttribute('hide-labels', hideLabels); - chart.toggleAttribute('round-corners', roundCorners); - - if (!chart.isConnected) { - container.appendChild(chart); - } - }; - - const roundedCornersControl = createSwitchField( - 'Rounded corners', - 'donut-rounded-corners', - roundCorners, - nextChecked => { - roundCorners = nextChecked; - roundedCornersControl.setValue(nextChecked); - renderChart(); - }, - ); - controls.appendChild(roundedCornersControl.element); - - const hideLabelsControl = createSwitchField('Hide labels', 'donut-rounded-hide-labels', hideLabels, nextChecked => { - hideLabels = nextChecked; - hideLabelsControl.setValue(nextChecked); - renderChart(); - }); - controls.appendChild(hideLabelsControl.element); - - renderChart(); - - return container; -}; - -export const HideLegends: Story = renderComponent(html>` - - -`); - -export const ShowLabelsInPercent: Story = () => { - const chart = document.createElement('fluent-donut-chart') as FluentDonutChart; - chart.setAttribute('chart-title', 'Donut chart percent labels example'); - chart.setAttribute('data', JSON.stringify(data)); - chart.setAttribute('inner-radius', '55'); - chart.toggleAttribute('show-labels-in-percent', true); - chart.hideLabels = false; - - return chart; -}; - -export const RTL: Story = renderComponent(html>` -
- - -
-`); diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts index 9e08fd8a11..46d650e6d8 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts @@ -9,7 +9,6 @@ import { colorStrokeFocus2, colorTransparentStroke, display, - forcedColorsStylesheetBehavior, spacingHorizontalL, spacingHorizontalNone, spacingHorizontalS, @@ -162,8 +161,8 @@ export const styles = css` .tooltip-content-y { ${typographyTitle2Styles} } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` + + @media (forced-colors: active) { .text-inside-donut { fill: CanvasText; } @@ -182,5 +181,5 @@ export const styles = css` forced-color-adjust: auto; color: CanvasText; } - `), -); + } +` diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.ts index 2c430b6ad7..512f1840e6 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.ts @@ -2,7 +2,6 @@ import { attr, FASTElement, nullableNumberConverter, observable } from '@microso import { format as d3Format } from 'd3-format'; import { arc as d3Arc, pie as d3Pie, PieArcDatum } from 'd3-shape'; import { - booleanStringConverter, getColorFromToken, getNextColor, getRTL, @@ -60,7 +59,7 @@ export class DonutChart extends FASTElement { public hideTooltip: boolean = false; @attr({ attribute: 'hide-labels', mode: 'boolean' }) - public hideLabels: boolean = true; + public hideLabels: boolean = false; @attr({ attribute: 'show-labels-in-percent', mode: 'boolean' }) public showLabelsInPercent: boolean = false; @@ -182,18 +181,53 @@ export class DonutChart extends FASTElement { } connectedCallback() { - this._initializeFromAttributes(); - - const initialChartData = this.data ? this._prepareChartData() : undefined; + // Class field initializers create own data properties that shadow the FAST @attr + // and @observable reactive getter/setters on the prototype. Delete them so that + // attribute changes go through the FAST reactive system and trigger the *Changed() + // callbacks, and so that observable assignments notify template bindings. + // We save the default values first so we can restore them for fields that have + // no corresponding HTML attribute (FAST won't call the setter in that case). + const self = this as Record; + const attrFields = [ + 'chartTitle', 'height', 'width', 'hideLegends', 'hideTooltip', 'hideLabels', + 'showLabelsInPercent', 'roundCorners', 'data', 'innerRadius', 'valueInsideDonut', + 'legendListLabel', 'order', 'culture', 'allowMultipleLegendSelection', + ] as const; + const observableFields = [ + 'tooltipProps', 'legends', 'activeLegend', 'isLegendSelected', 'selectedLegends', + ] as const; + const saved: Partial> = {}; + const savedObservables: Partial> = {}; + for (const field of attrFields) { + saved[field] = self[field]; + delete self[field]; + } + for (const field of observableFields) { + savedObservables[field] = self[field]; + delete self[field]; + // Restore observable defaults through the prototype's FAST reactive setter + // BEFORE super.connectedCallback() renders the template. + if (savedObservables[field] !== undefined) { + self[field] = savedObservables[field]; + } + } super.connectedCallback(); + // Restore defaults for any attr-backed field that was not set from an HTML attribute. + for (const field of attrFields) { + if (self[field] === undefined && saved[field] !== undefined) { + self[field] = saved[field]; + } + } + this.addEventListener('mouseleave', this._handleMouseLeave); - if (!this.data || !initialChartData) { + if (!this.data) { return; } + const initialChartData = this._prepareChartData(); this._isRTL = getRTL(this); this._render(initialChartData); } @@ -343,69 +377,6 @@ export class DonutChart extends FASTElement { this._textInsideDonut = undefined; } - private _initializeFromAttributes() { - const setString = (name: string, assign: (value: string) => void) => { - const value = this.getAttribute(name); - if (value !== null) { - assign(value); - } - }; - - const setBoolean = (name: string, assign: (value: boolean) => void) => { - const value = this.getAttribute(name); - if (value !== null) { - assign(booleanStringConverter.fromView(value)); - } - }; - - setString('chart-title', value => { - this.chartTitle = value; - }); - setString('height', value => { - this.height = nullableNumberConverter.fromView(value) ?? this.height; - }); - setString('width', value => { - this.width = nullableNumberConverter.fromView(value) ?? this.width; - }); - setString('data', value => { - this.data = jsonConverter.fromView(value) as ChartProps; - }); - setString('inner-radius', value => { - this.innerRadius = nullableNumberConverter.fromView(value) ?? this.innerRadius; - }); - setString('value-inside-donut', value => { - this.valueInsideDonut = value; - }); - setString('legend-list-label', value => { - this.legendListLabel = value; - }); - setString('order', value => { - this.order = value as 'default' | 'sorted'; - }); - setString('culture', value => { - this.culture = value; - }); - - setBoolean('hide-legends', value => { - this.hideLegends = value; - }); - setBoolean('hide-tooltip', value => { - this.hideTooltip = value; - }); - setBoolean('hide-labels', value => { - this.hideLabels = value; - }); - setBoolean('show-labels-in-percent', value => { - this.showLabelsInPercent = value; - }); - setBoolean('round-corners', value => { - this.roundCorners = value; - }); - setBoolean('allow-multiple-legend-selection', value => { - this.allowMultipleLegendSelection = value; - }); - } - private _initializeAndRender() { const chartData = this._prepareChartData(); @@ -481,7 +452,8 @@ export class DonutChart extends FASTElement { path.setAttribute('role', 'img'); path.addEventListener('mouseover', event => { - if (this.activeLegend !== '' && this.activeLegend !== arcDatum.data.legend) { + const highlighted = this._getHighlightedLegends(); + if (highlighted.length > 0 && !highlighted.includes(arcDatum.data.legend)) { return; } @@ -490,14 +462,15 @@ export class DonutChart extends FASTElement { this._setTooltipProps({ isVisible: true, legend: arcDatum.data.legend, - yValue: `${arcDatum.data.data}`, + yValue: this._formatDataPointValue(arcDatum.data), color: arcDatum.data.color!, xPos: this._isRTL ? bounds.right - event.clientX : event.clientX - bounds.left, yPos: event.clientY - bounds.top - 85, }); }); path.addEventListener('focus', event => { - if (this.activeLegend !== '' && this.activeLegend !== arcDatum.data.legend) { + const highlighted = this._getHighlightedLegends(); + if (highlighted.length > 0 && !highlighted.includes(arcDatum.data.legend)) { return; } @@ -507,7 +480,7 @@ export class DonutChart extends FASTElement { this._setTooltipProps({ isVisible: true, legend: arcDatum.data.legend, - yValue: `${arcDatum.data.data}`, + yValue: this._formatDataPointValue(arcDatum.data), color: arcDatum.data.color!, xPos: this._isRTL ? rootBounds.right - arcBounds.left - arcBounds.width / 2 @@ -719,6 +692,11 @@ export class DonutChart extends FASTElement { return formatted.endsWith('K') ? `${formatted.slice(0, -1)}k` : formatted; } + private _formatDataPointValue(dataPoint: ChartDataPoint): string { + return dataPoint.yAxisCalloutData ?? dataPoint.calloutData ?? + dataPoint.data.toLocaleString(this.culture || undefined); + } + private _getTextInsideDonut(valueInsideDonut: string) { let textInsideDonut = valueInsideDonut; @@ -735,10 +713,7 @@ export class DonutChart extends FASTElement { dataPoint => dataPoint.legend === singleHighlight, ); if (highlightedDataPoint) { - textInsideDonut = - highlightedDataPoint.yAxisCalloutData ?? - highlightedDataPoint.calloutData ?? - highlightedDataPoint.data.toLocaleString(this.culture || undefined); + textInsideDonut = this._formatDataPointValue(highlightedDataPoint); } } diff --git a/src/Charts.Scripts/src/helpers.stories.ts b/src/Charts.Scripts/src/helpers.stories.ts deleted file mode 100644 index a568c4b82b..0000000000 --- a/src/Charts.Scripts/src/helpers.stories.ts +++ /dev/null @@ -1,101 +0,0 @@ -import type { ElementViewTemplate, FASTElement, ViewTemplate } from '@microsoft/fast-element'; -import type { AnnotatedStoryFn, Args, ComponentAnnotations, Renderer, StoryAnnotations } from 'storybook/internal/csf'; - -/** - * A helper that returns a function to bind a Storybook story to a ViewTemplate. - * - * @param template - The ViewTemplate to render - * @returns - a function to bind a Storybook story - */ -export function renderComponent(template: ViewTemplate): (args: TArgs) => Element | DocumentFragment { - return function (args) { - const storyFragment = new DocumentFragment(); - template.render(args, storyFragment); - if (storyFragment.childElementCount === 1) { - return storyFragment.firstElementChild!; - } - return storyFragment; - }; -} - -export declare interface FASTComponentsRenderer extends Renderer { - canvasElement: FASTElement; - component: typeof FASTElement | string; - storyResult: string | Node | DocumentFragment | ElementViewTemplate; -} - -/** - * A helper that returns a function to bind a Storybook story to a ViewTemplate. - */ -export type FASTFramework = Renderer & { - component: typeof FASTElement; - storyResult: FASTElement | Element | DocumentFragment; -}; - -/** - * Metadata to configure the stories for a component. - */ -export declare type Meta = ComponentAnnotations>; - -/** - * Story object that represents a CSFv3 component example. - * - * @see [Named Story exports](https://storybook.js.org/docs/formats/component-story-format/#named-story-exports) - */ -export declare type StoryObj = StoryAnnotations>; - -/** - * Story function that represents a CSFv2 component example. - */ -export declare type StoryFn = AnnotatedStoryFn; - -/** - * Story function that represents a CSFv2 component example. - * - * NOTE that in Storybook 7.0, this type will be renamed to `StoryFn` and replaced by the current `StoryObj` type. - */ -export declare type Story = StoryFn>; - -/** - * Combined Storybook story args. - */ -export type StoryArgs = Partial> & Args; - -export function generateImage({ - width, - height = width, - backgroundColor = 'rgb(204, 204, 204)', - color = 'rgb(150, 150, 150)', - text = `${width} x ${height}`, -}: { - width: number; - height?: number; - backgroundColor?: string; - color?: string; - text?: string; -}): string { - const canvas = document.createElement('canvas'); - const context = canvas.getContext('2d') as CanvasRenderingContext2D; - - canvas.width = width; - canvas.height = height; - - // Clear the canvas. - context.clearRect(0, 0, canvas.width, canvas.height); - - // get the font size to fit the text - context.font = '1px sans-serif'; - const maxFontSize = Math.max(width / context.measureText(text).width / 2, 7); - - // Draw the background - context.fillStyle = backgroundColor; - context.fillRect(0, 0, canvas.width, canvas.height); - - context.font = `${maxFontSize}px Helvetica, Arial, sans-serif`; - context.textAlign = 'center'; - context.textBaseline = 'middle'; - context.fillStyle = color; - context.fillText(text, canvas.width / 2, canvas.height / 2); - - return canvas.toDataURL('image/png'); -} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts deleted file mode 100644 index 31f68df997..0000000000 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.bench.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { FluentDesignSystem } from '@fluentui/web-components'; -import { definition } from './horizontal-bar-chart-with-axis.definition.js'; - -definition.define(FluentDesignSystem.registry); - -const itemRenderer = () => { - const chart = document.createElement('fluent-horizontal-bar-chart-with-axis'); - return chart; -}; - -export default itemRenderer; -export { tests } from '../utils/benchmark-wrapper.js'; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts index 376f23d78b..a717e89bde 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.styles.ts @@ -8,7 +8,6 @@ import { colorNeutralStrokeAccessible, display, fontSizeHero700, - forcedColorsStylesheetBehavior, shadow4, spacingHorizontalL, spacingHorizontalNone, @@ -164,9 +163,9 @@ export const styles: ElementStyles = css` direction: ltr; unicode-bidi: isolate; } -`.withBehaviors( - forcedColorsStylesheetBehavior(css` - .legend-rect, + + @media (forced-colors: active) { + .legend-rect, .tooltip-info { forced-color-adjust: none; } @@ -174,5 +173,5 @@ export const styles: ElementStyles = css` .bar-label { fill: CanvasText !important; } - `), -); + } +`; diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts index b14afdacf6..9e6f87e886 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.template.ts @@ -12,14 +12,14 @@ export function horizontalBarChartWithAxisTemplate`
${repeat( - x => x.uniqueLegends, + x => x.legends, html`
[Parameter] public string? LegendListLabel { get; set; } + + /// + /// Gets or sets whether to use a single color for all bars in the chart. + /// + [Parameter] + public bool UseSingleColor { get; set; } + + /// + /// Gets or sets whether gradient rendering is enabled. + /// + [Parameter] + public bool EnableGradient { get; set; } + + /// + /// Gets or sets whether the bars in the chart should have rounded corners. + /// + [Parameter] + public bool RoundedCorners { get; set; } + + /// + /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. + /// When , clicking a legend item adds it to the active selection rather than replacing the current selection. + /// When (default), only a single legend item can be selected at a time. + /// + [Parameter] + public bool AllowMultipleLegendSelection { get; set; } } From 016fc4755db4b55126113a2ac0cecec046efb0ab Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Wed, 6 May 2026 09:34:14 +0200 Subject: [PATCH 10/60] - Add coercion step for parsing axis values - Ignore playwright screenshots and chart-comments files --- .gitignore | 2 ++ .../horizontal-bar-chart-with-axis.ts | 7 +++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 8ec35f9708..803e46c7fd 100644 --- a/.gitignore +++ b/.gitignore @@ -426,3 +426,5 @@ all-emojis.json /global.json /src/Core.Scripts/src/BuildConstants.ts /src/Core.Scripts/obj/ +/.playwright-mcp +/examples/Demo/FluentUI.Demo.Client/wwwroot/chart-comments.json diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts index 4b79b491f4..f5e1b3fe86 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts @@ -728,12 +728,15 @@ export class HorizontalBarChartWithAxis extends FASTElement { private _getGroupedSeries(): GroupedSeries[] { const groups = new Map(); this.data.forEach(point => { - const key = String(point.y); + const numericCandidate = typeof point.y === 'string' ? Number(point.y) : NaN; + const rawY: number | string = + typeof point.y === 'number' ? point.y : isFinite(numericCandidate) ? numericCandidate : point.y; + const key = String(rawY); const existing = groups.get(key); if (existing) { existing.points.push(point); } else { - groups.set(key, { key, rawY: point.y, points: [point] }); + groups.set(key, { key, rawY, points: [point] }); } }); From 8ba166c79d60a4c2627c6fcf4b7261c872659bc2 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 11:25:29 +0200 Subject: [PATCH 11/60] Ignore generated chart-comments.json --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 803e46c7fd..5caceae526 100644 --- a/.gitignore +++ b/.gitignore @@ -420,6 +420,7 @@ Microsoft.FluentUI.AspNetCore.Components.Charts.xml /examples/Demo/FluentUI.Demo.Client/wwwroot/sources/ /examples/Demo/FluentUI.Demo.Client/wwwroot/documentation/ api-comments.json +chart-comments.json mcp-documentation.json all-icons.json all-emojis.json @@ -427,4 +428,3 @@ all-emojis.json /src/Core.Scripts/src/BuildConstants.ts /src/Core.Scripts/obj/ /.playwright-mcp -/examples/Demo/FluentUI.Demo.Client/wwwroot/chart-comments.json From 28e72491980a3b46afb37fe50b50380e24bef127 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 12:30:42 +0200 Subject: [PATCH 12/60] - Enhance DataVizPalette - Fix HBCWA color handling - Fix HBCWA Blazor example --- .../HorizontalBarChartWithAxisDefault.razor | 10 +--- .../horizontal-bar-chart-with-axis.ts | 35 +++++++---- src/Charts.Scripts/src/utils/chart-helpers.ts | 60 ++++++++++++++++--- 3 files changed, 76 insertions(+), 29 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor index 40074201d1..15419a3cf2 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDefault.razor @@ -4,15 +4,7 @@ ChartTitle="Default Horizontal Bar Chart" ChartData="@ChartData" Width="650" - height="350" - HideLegends="false" - HideTooltip="false" - HideLabels="false" - show-y-axis-labels="false" - show-y-axis-labels-tooltip="false" - use-single-color="false" - enable-gradient="false" - y-axis-category-order="default"> + Height="350"> @code { diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts index f5e1b3fe86..35e480cbad 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts @@ -596,7 +596,8 @@ export class HorizontalBarChartWithAxis extends FASTElement { this._renderOriginLine(axisLayer, plotLayout.margins, height, xAxisScale.domain, innerWidth); this._renderedBars = []; - const legendColorMap = new Map(); + const legendColorMap = this._buildLegendColorMap(); + let globalPointIndex = 0; const scaleX = (value: number) => { const [min, max] = xAxisScale.domain; const safeSpan = max - min || 1; @@ -615,13 +616,9 @@ export class HorizontalBarChartWithAxis extends FASTElement { let positivePointIndex = 0; let negativePointIndex = 0; - group.points.forEach((point, pointIndex) => { - const color = this._getPointColor(point, pointIndex); - const gradientId = this._appendGradient(defs, groupIndex, pointIndex, point, color); - - if (point.legend && !legendColorMap.has(point.legend)) { - legendColorMap.set(point.legend, color); - } + group.points.forEach((point, _pointIndex) => { + const color = this._getPointColor(point, globalPointIndex++, legendColorMap); + const gradientId = this._appendGradient(defs, groupIndex, globalPointIndex - 1, point, color); const startValue = point.x >= 0 ? positiveTotal : negativeTotal; const endValue = startValue + point.x; @@ -687,8 +684,7 @@ export class HorizontalBarChartWithAxis extends FASTElement { }); this.legends = Array.from(legendColorMap.entries()).map(([legend, color]) => ({ legend, color })); - this.chartContainer.appendChild(svg); - this._updateLegendInteractionState(); + this.chartContainer.appendChild(svg); this._updateLegendInteractionState(); } private _clearChart() { @@ -943,7 +939,20 @@ export class HorizontalBarChartWithAxis extends FASTElement { ); } - private _getPointColor(point: HorizontalBarChartWithAxisDataPoint, index: number) { + private _buildLegendColorMap(): Map { + const map = new Map(); + let index = 0; + this.data.forEach(point => { + const legend = point.legend; + if (legend && !map.has(legend)) { + map.set(legend, point.color ? getColorFromToken(point.color) : getNextColor(index, 0)); + index++; + } + }); + return map; + } + + private _getPointColor(point: HorizontalBarChartWithAxisDataPoint, index: number, legendColorMap?: Map) { if (this.useSingleColor) { const singleColorPoint = this.data.find( candidate => typeof candidate.color === 'string' && candidate.color.length > 0, @@ -955,6 +964,10 @@ export class HorizontalBarChartWithAxis extends FASTElement { return getColorFromToken(point.color); } + if (point.legend && legendColorMap?.has(point.legend)) { + return legendColorMap.get(point.legend)!; + } + return getNextColor(index, 0); } diff --git a/src/Charts.Scripts/src/utils/chart-helpers.ts b/src/Charts.Scripts/src/utils/chart-helpers.ts index 999f19a9aa..581349f1c5 100644 --- a/src/Charts.Scripts/src/utils/chart-helpers.ts +++ b/src/Charts.Scripts/src/utils/chart-helpers.ts @@ -82,15 +82,36 @@ export const DataVizPalette = { color8: 'qualitative.8', color9: 'qualitative.9', color10: 'qualitative.10', - color11: 'qualitative.21', - color12: 'qualitative.22', - color13: 'qualitative.23', - color14: 'qualitative.24', - color15: 'qualitative.25', - color16: 'qualitative.26', - color17: 'qualitative.27', - color18: 'qualitative.28', - color19: 'qualitative.29', + color11: 'qualitative.11', + color12: 'qualitative.12', + color13: 'qualitative.13', + color14: 'qualitative.14', + color15: 'qualitative.15', + color16: 'qualitative.16', + color17: 'qualitative.17', + color18: 'qualitative.18', + color19: 'qualitative.19', + color20: 'qualitative.20', + color21: 'qualitative.21', + color22: 'qualitative.22', + color23: 'qualitative.23', + color24: 'qualitative.24', + color25: 'qualitative.25', + color26: 'qualitative.26', + color27: 'qualitative.27', + color28: 'qualitative.28', + color29: 'qualitative.29', + color30: 'qualitative.30', + color31: 'qualitative.31', + color32: 'qualitative.32', + color33: 'qualitative.33', + color34: 'qualitative.34', + color35: 'qualitative.35', + color36: 'qualitative.36', + color37: 'qualitative.37', + color38: 'qualitative.38', + color39: 'qualitative.39', + color40: 'qualitative.40', info: 'semantic.info', disabled: 'semantic.disabled', highError: 'semantic.highError', @@ -119,6 +140,16 @@ const QualitativePalette: Palette = { '8': ['#57811b'], // [lime.shade20], '9': ['#b146c2'], // [lilac.primary], '10': ['#ae8c00'], // [gold.shade10], + '11': ['#3c51b4', '#93a4f4'], // [cornflower.shade20, cornflower.tint30], + '12': ['#ad006a', '#ee5fb7'], // [hotPink.shade20, hotPink.tint30], + '13': ['#026467', '#4cb4b7'], // [teal.shade20, teal.tint30], + '14': ['#674c8c', '#a083c9'], // [orchid.shade20, orchid.tint20], + '15': ['#0e7a0b', '#27ac22'], // [lightGreen.shade20, lightGreen.tint10], + '16': ['#2c72a8', '#4fa1e1'], // [lightBlue.shade20, lightBlue.tint10], + '17': ['#9a3d0c', '#d77440'], // [pumpkin.shade20, pumpkin.tint20], + '18': ['#405f14', '#73aa24'], // [lime.shade30, lime.primary], + '19': ['#863593', '#c36bd1'], // [lilac.shade20, lilac.tint20], + '20': ['#6d5700', '#d0b232'], // [gold.shade30, gold.tint20], '21': ['#4f6bed'], // [cornflower.primary], '22': ['#ea38a6'], // [hotPink.tint20], '23': ['#038387'], // [teal.primary], @@ -128,6 +159,17 @@ const QualitativePalette: Palette = { '27': ['#d06228'], // [pumpkin.tint10], '28': ['#689920'], // [lime.shade10], '29': ['#ba58c9'], // [lilac.tint10], + '30': ['#937700', '#c19c00'], // [gold.shade20, gold.primary], + '31': ['#2c3c85', '#c8d1fa'], // [cornflower.shade30, cornflower.tint40], + '32': ['#7f004e', '#f7adda'], // [hotPink.shade30, hotPink.tint40], + '33': ['#02494c', '#9bd9db'], // [teal.shade30, teal.tint40], + '34': ['#4c3867', '#b29ad4'], // [orchid.shade30, orchid.tint30], + '35': ['#0b5a08', '#a7e3a5'], // [lightGreen.shade30, lightGreen.tint40], + '36': ['#20547c', '#83bdeb'], // [lightBlue.shade30, lightBlue.tint30], + '37': ['#712d09', '#df8e64'], // [pumpkin.shade30, pumpkin.tint30], + '38': ['#23330b', '#a4cc6c'], // [lime.shade40, lime.tint30], + '39': ['#63276d', '#cf87da'], // [lilac.shade30, lilac.tint30], + '40': ['#3a2f00', '#dac157'], // [gold.shade40, gold.tint30], }; const SemanticPalette: Palette = { From e7e08aba3f5e9b1bd10d590b5ec7151df695cb20 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 13:04:19 +0200 Subject: [PATCH 13/60] Addd missing parameters on the Blazor side --- .../FluentHorizontalBarChartWithAxis.razor | 16 +++- .../FluentHorizontalBarChartWithAxis.razor.cs | 81 +++++++++++++++++++ 2 files changed, 94 insertions(+), 3 deletions(-) diff --git a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor index 74ea0100c1..9ccccf00e7 100644 --- a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor +++ b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor @@ -1,4 +1,5 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Extensions @inherits FluentComponentBase diff --git a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs index d0a4aad1e4..8acdbe9430 100644 --- a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs +++ b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor.cs @@ -2,7 +2,9 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using System.Globalization; using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Enums; using Microsoft.FluentUI.AspNetCore.Components.Utilities; namespace Microsoft.FluentUI.AspNetCore.Components.Charts; @@ -100,4 +102,83 @@ public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : ba ///
[Parameter] public bool AllowMultipleLegendSelection { get; set; } + + /// + /// Gets or sets a value indicating whether the full Y-axis labels are shown. + /// When (default), long labels are truncated. + /// + [Parameter] + public bool ShowYAxisLabels { get; set; } + + /// + /// Gets or sets a value indicating whether a tooltip is shown on Y-axis labels when they are truncated. + /// + [Parameter] + public bool ShowYAxisLabelsTooltip { get; set; } + + /// + /// Gets or sets the sort order applied to categorical Y-axis groups. + /// Defaults to . + /// + [Parameter] + public HorizontalBarChartWithAxisCategoryOrder YAxisCategoryOrder { get; set; } = HorizontalBarChartWithAxisCategoryOrder.Default; + + /// + /// Gets or sets the fixed height of each individual bar in pixels. + /// When not set, the bar height is calculated automatically. + /// + [Parameter] + public int? BarHeight { get; set; } + + /// + /// Gets or sets the number of tick marks on the X axis. + /// + [Parameter] + public int? XAxisTickCount { get; set; } + + /// + /// Gets or sets the number of tick marks on the Y axis (numeric axis only). + /// + [Parameter] + public int? YAxisTickCount { get; set; } + + /// + /// Gets or sets the fractional padding (0–1) between bars on the categorical Y axis. + /// Defaults to 0.5. + /// + [Parameter] + public double? YAxisPadding { get; set; } + + /// + /// Gets or sets the minimum value of the X axis domain. + /// When not set, the domain minimum is derived from the data. + /// + [Parameter] + public double? XMinValue { get; set; } + + /// + /// Gets or sets the maximum value of the X axis domain. + /// When not set, the domain maximum is derived from the data. + /// + [Parameter] + public double? XMaxValue { get; set; } + + /// + /// Gets or sets the minimum value of the Y axis domain (numeric axis only). + /// + [Parameter] + public double? YMinValue { get; set; } + + /// + /// Gets or sets the maximum value of the Y axis domain (numeric axis only). + /// + [Parameter] + public double? YMaxValue { get; set; } + + /// + /// Gets or sets the culture used for locale-aware number formatting of values in the chart. + /// Defaults to to display using the OS culture. + /// + [Parameter] + public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; } From f2acc8d8dbe7185b33c59c88f995df1d24b1f16c Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 17:29:55 +0200 Subject: [PATCH 14/60] Add more examples --- .../DonutChart/DonutChartCulture.razor | 20 +++ .../DonutChart/DonutChartHideTooltip.razor | 23 +++ .../DonutChartLegendListLabel.razor | 19 +++ .../DonutChartMultipleLegendSelection.razor | 23 +++ .../DonutChart/DonutChartOutsideLabels.razor | 21 +++ .../DonutChartValueInsideDonut.razor | 25 ++++ ...izontalBarChartChartDataModeFraction.razor | 23 +++ ...ntalBarChartChartDataModeInteractive.razor | 35 +++++ ...ontalBarChartChartDataModePercentage.razor | 23 +++ .../HorizontalBarChartCulture.razor | 47 +++++++ .../HorizontalBarChartGradient.razor | 50 +++++++ .../HorizontalBarChartHideLabels.razor | 46 ++++++ .../HorizontalBarChartHideLegends.razor | 50 +++++++ .../HorizontalBarChartHideRatio.razor | 23 +++ .../HorizontalBarChartHideTooltip.razor | 50 +++++++ .../HorizontalBarChartLegendListLabel.razor | 46 ++++++ ...ontalBarChartMultipleLegendSelection.razor | 50 +++++++ .../HorizontalBarChartRoundedCorners.razor | 27 ++++ ...zontalBarChartWithAxisAxisTickCounts.razor | 34 +++++ .../HorizontalBarChartWithAxisBarHeight.razor | 25 ++++ ...izontalBarChartWithAxisCategoryOrder.razor | 43 ++++++ .../HorizontalBarChartWithAxisCulture.razor | 18 +++ ...zontalBarChartWithAxisDomainOverride.razor | 48 +++++++ .../HorizontalBarChartWithAxisGradient.razor | 17 +++ ...HorizontalBarChartWithAxisHideLabels.razor | 17 +++ ...orizontalBarChartWithAxisHideLegends.razor | 17 +++ ...orizontalBarChartWithAxisHideTooltip.razor | 23 +++ ...ontalBarChartWithAxisLegendListLabel.razor | 17 +++ ...ChartWithAxisMultipleLegendSelection.razor | 28 ++++ .../HorizontalBarChartWithAxisNegative.razor | 56 ++++++++ ...rizontalBarChartWithAxisNumericYAxis.razor | 17 +++ .../HorizontalBarChartWithAxisRTL.razor | 18 +++ ...zontalBarChartWithAxisRoundedCorners.razor | 17 +++ ...ontalBarChartWithAxisShowYAxisLabels.razor | 17 +++ ...orizontalBarChartWithAxisSingleColor.razor | 17 +++ .../HorizontalBarChartWithAxisStacked.razor | 21 +++ ...orizontalBarChartWithAxisStringYAxis.razor | 16 +++ ...rizontalBarChartWithAxisYAxisPadding.razor | 26 ++++ .../Charts/Pages/FluentDonutChartPage.md | 52 ++++++- .../Pages/FluentHorizontalBarChartPage.md | 84 +++++++++++ .../Pages/FluentHorizontalBarChartWithAxis.md | 132 ++++++++++++++++++ .../horizontal-bar-chart-with-axis.ts | 3 +- .../FluentHorizontalBarChart.razor | 6 + .../FluentHorizontalBarChart.razor.cs | 37 +++++ .../HorizontalBarChartModels.cs | 0 src/Charts/Models/HorizontalBarChartModels.cs | 1 + 46 files changed, 1406 insertions(+), 2 deletions(-) create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartCulture.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideTooltip.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLegendListLabel.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartMultipleLegendSelection.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartValueInsideDonut.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeFraction.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeInteractive.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModePercentage.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartCulture.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartGradient.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLabels.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLegends.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideRatio.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideTooltip.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartLegendListLabel.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartMultipleLegendSelection.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartRoundedCorners.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisAxisTickCounts.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisBarHeight.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCategoryOrder.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCulture.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDomainOverride.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisGradient.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLabels.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLegends.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideTooltip.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisLegendListLabel.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisMultipleLegendSelection.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNegative.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNumericYAxis.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRoundedCorners.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisShowYAxisLabels.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSingleColor.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStacked.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStringYAxis.razor create mode 100644 examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisYAxisPadding.razor create mode 100644 src/Charts/Charts/HorizontalBarChart/HorizontalBarChartModels.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartCulture.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartCulture.razor new file mode 100644 index 0000000000..5b579084b6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartCulture.razor @@ -0,0 +1,20 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using System.Globalization + + + +@code { + private DonutChartData _data = new() + { + ChartTitle = "Donut chart culture example (de-DE)", + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideTooltip.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideTooltip.razor new file mode 100644 index 0000000000..130b0faa94 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartHideTooltip.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + +@code { + private bool _hideTooltip = true; + + private DonutChartData _data = new() + { + ChartTitle = "Donut chart hide tooltip example", + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLegendListLabel.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLegendListLabel.razor new file mode 100644 index 0000000000..f42c8f5b0e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartLegendListLabel.razor @@ -0,0 +1,19 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + +@code { + private DonutChartData _data = new() + { + ChartTitle = "Donut chart legend list label example", + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartMultipleLegendSelection.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartMultipleLegendSelection.razor new file mode 100644 index 0000000000..93928b7c5b --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartMultipleLegendSelection.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + +@code { + private bool _allowMultiple = true; + + private DonutChartData _data = new() + { + ChartTitle = "Donut chart multiple legend selection example", + ChartData = + [ + new DonutChartDataPoint { Legend = "small", Data = 5000 }, + new DonutChartDataPoint { Legend = "large", Data = 39000 }, + new DonutChartDataPoint { Legend = "medium", Data = 15000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor new file mode 100644 index 0000000000..0fa20c5eb0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor @@ -0,0 +1,21 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + +@code { + private DonutChartData _data = new() + { + ChartTitle = "Donut chart outside labels example", + ChartData = + [ + new DonutChartDataPoint { Legend = "first", Data = 20000 }, + new DonutChartDataPoint { Legend = "second", Data = 39000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartValueInsideDonut.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartValueInsideDonut.razor new file mode 100644 index 0000000000..80d3b2c07e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartValueInsideDonut.razor @@ -0,0 +1,25 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ + + +@code { + private string? _value = "39,000"; + + private DonutChartData _data = new() + { + ChartTitle = "Donut chart value inside donut example", + ChartData = + [ + new DonutChartDataPoint { Legend = "small", Data = 5000 }, + new DonutChartDataPoint { Legend = "large", Data = 39000 }, + new DonutChartDataPoint { Legend = "medium", Data = 15000 }, + ] + }; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeFraction.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeFraction.razor new file mode 100644 index 0000000000..55c5465caf --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeFraction.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries { ChartSeriesTitle = "one", ChartData = [ new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "two", ChartData = [ new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "three", ChartData = [ new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "four", ChartData = [ new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "five", ChartData = [ new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "six", ChartData = [ new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "seven", ChartData = [ new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "eight", ChartData = [ new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } ] }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeInteractive.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeInteractive.razor new file mode 100644 index 0000000000..fed0ef9999 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModeInteractive.razor @@ -0,0 +1,35 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + default + fraction + percentage + +
+ + + + +@code { + private string? _chartDataMode = "default"; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries { ChartSeriesTitle = "one", ChartData = [ new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "two", ChartData = [ new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "three", ChartData = [ new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "four", ChartData = [ new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "five", ChartData = [ new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "six", ChartData = [ new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "seven", ChartData = [ new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "eight", ChartData = [ new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } ] }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModePercentage.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModePercentage.razor new file mode 100644 index 0000000000..5f11c78f4d --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartChartDataModePercentage.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries { ChartSeriesTitle = "one", ChartData = [ new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "two", ChartData = [ new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "three", ChartData = [ new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "four", ChartData = [ new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "five", ChartData = [ new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "six", ChartData = [ new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "seven", ChartData = [ new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "eight", ChartData = [ new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } ] }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartCulture.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartCulture.razor new file mode 100644 index 0000000000..f859709bf6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartCulture.razor @@ -0,0 +1,47 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using System.Globalization + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartGradient.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartGradient.razor new file mode 100644 index 0000000000..272913fe03 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartGradient.razor @@ -0,0 +1,50 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + +@code { + private bool _enableGradient = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLabels.razor new file mode 100644 index 0000000000..82a36cc734 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLabels.razor @@ -0,0 +1,46 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLegends.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLegends.razor new file mode 100644 index 0000000000..9b943d9ae6 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideLegends.razor @@ -0,0 +1,50 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + +@code { + private bool _hideLegends = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideRatio.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideRatio.razor new file mode 100644 index 0000000000..5714dcc9f0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideRatio.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries { ChartSeriesTitle = "one", ChartData = [ new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "two", ChartData = [ new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "three", ChartData = [ new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "four", ChartData = [ new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "five", ChartData = [ new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "six", ChartData = [ new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "seven", ChartData = [ new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "eight", ChartData = [ new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } ] }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideTooltip.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideTooltip.razor new file mode 100644 index 0000000000..818665cd24 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartHideTooltip.razor @@ -0,0 +1,50 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + +@code { + private bool _hideTooltip = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartLegendListLabel.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartLegendListLabel.razor new file mode 100644 index 0000000000..642e75191c --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartLegendListLabel.razor @@ -0,0 +1,46 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartMultipleLegendSelection.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartMultipleLegendSelection.razor new file mode 100644 index 0000000000..ea6c393e90 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartMultipleLegendSelection.razor @@ -0,0 +1,50 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + + + +@code { + private bool _allowMultiple = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored First", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 23, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 87, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Monitored Second", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Debit card numbers (EU and USA)", Data = 40, Color = "#0099BC" }, + new HorizontalBarChartDataPoint { Legend = "Passport numbers (USA)", Data = 56, Color = "#77004D" }, + new HorizontalBarChartDataPoint { Legend = "Social security numbers", Data = 35, Color = "#4F68ED" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 92, Color = "#AE8C00" }, + new HorizontalBarChartDataPoint { Legend = "Tax identification numbers (USA)", Data = 87, Color = "#004E8C" }, + ] + }, + new HorizontalBarChartSeries + { + ChartSeriesTitle = "Unmonitored", + ChartData = + [ + new HorizontalBarChartDataPoint { Legend = "Phone Numbers", Data = 40, Color = "#881798" }, + new HorizontalBarChartDataPoint { Legend = "Credit card Numbers", Data = 23, Color = "#AE8C00" }, + ] + }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartRoundedCorners.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartRoundedCorners.razor new file mode 100644 index 0000000000..4e72a44a9b --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChart/HorizontalBarChartRoundedCorners.razor @@ -0,0 +1,27 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using Microsoft.FluentUI.AspNetCore.Components.Enums + + + + + + +@code { + private bool _roundCorners = false; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartSeries { ChartSeriesTitle = "one", ChartData = [ new HorizontalBarChartDataPoint { Legend = "one", Data = 1543, Total = 15000, Color = "#637cef" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "two", ChartData = [ new HorizontalBarChartDataPoint { Legend = "two", Data = 800, Total = 15000, Color = "#e3008c" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "three", ChartData = [ new HorizontalBarChartDataPoint { Legend = "three", Data = 8888, Total = 15000, Color = "#2aa0a4" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "four", ChartData = [ new HorizontalBarChartDataPoint { Legend = "four", Data = 15888, Total = 15000, Color = "#9373c0" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "five", ChartData = [ new HorizontalBarChartDataPoint { Legend = "five", Data = 11444, Total = 15000, Color = "#13a10e" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "six", ChartData = [ new HorizontalBarChartDataPoint { Legend = "six", Data = 14000, Total = 15000, Color = "#3a96dd" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "seven", ChartData = [ new HorizontalBarChartDataPoint { Legend = "seven", Data = 9855, Total = 15000, Color = "#ca5010" } ] }, + new HorizontalBarChartSeries { ChartSeriesTitle = "eight", ChartData = [ new HorizontalBarChartDataPoint { Legend = "eight", Data = 4250, Total = 15000, Color = "#57811b" } ] }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisAxisTickCounts.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisAxisTickCounts.razor new file mode 100644 index 0000000000..f152d9789f --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisAxisTickCounts.razor @@ -0,0 +1,34 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+
+ +
+
+ +
+
+ + + + +@code { + private int _xTicks = 5; + private int _yTicks = 4; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "5000", Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "5K" }, + new HorizontalBarChartWithAxisDataPoint { X = 20000, Y = "50000", Legend = "Dogs", Color = "#e3008c", XAxisCalloutData = "20K", YAxisCalloutData = "50K" }, + new HorizontalBarChartWithAxisDataPoint { X = 25000, Y = "30000", Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "25K", YAxisCalloutData = "30K" }, + new HorizontalBarChartWithAxisDataPoint { X = 40000, Y = "13000", Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "40K", YAxisCalloutData = "13K" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisBarHeight.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisBarHeight.razor new file mode 100644 index 0000000000..13f3fad001 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisBarHeight.razor @@ -0,0 +1,25 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ + + + +@code { + private int _barHeight = 16; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCategoryOrder.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCategoryOrder.razor new file mode 100644 index 0000000000..7cc9d3750b --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCategoryOrder.razor @@ -0,0 +1,43 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + @foreach (var opt in _orderOptions) + { + @opt.ToString() + } + +
+ + + + +@code { + private Microsoft.FluentUI.AspNetCore.Components.Enums.HorizontalBarChartWithAxisCategoryOrder _order = Microsoft.FluentUI.AspNetCore.Components.Enums.HorizontalBarChartWithAxisCategoryOrder.Default; + + private static readonly Microsoft.FluentUI.AspNetCore.Components.Enums.HorizontalBarChartWithAxisCategoryOrder[] _orderOptions = + Enum.GetValues(); + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 80, Y = "Label 1", Legend = "Legend 1", Color = "#637cef" }, + new HorizontalBarChartWithAxisDataPoint { X = -30, Y = "Label 2", Legend = "Legend 2", Color = "#e3008c" }, + new HorizontalBarChartWithAxisDataPoint { X = 50, Y = "Label 1", Legend = "Legend 2", Color = "#e3008c" }, + new HorizontalBarChartWithAxisDataPoint { X = 20, Y = "Label 3", Legend = "Legend 1", Color = "#637cef" }, + new HorizontalBarChartWithAxisDataPoint { X = -60, Y = "Label 2", Legend = "Legend 1", Color = "#637cef" }, + new HorizontalBarChartWithAxisDataPoint { X = 40, Y = "Label 3", Legend = "Legend 3", Color = "#2aa0a4" }, + new HorizontalBarChartWithAxisDataPoint { X = -10, Y = "Label 1", Legend = "Legend 3", Color = "#2aa0a4" }, + new HorizontalBarChartWithAxisDataPoint { X = 70, Y = "Label 4", Legend = "Legend 2", Color = "#e3008c" }, + new HorizontalBarChartWithAxisDataPoint { X = -45, Y = "Label 4", Legend = "Legend 4", Color = "#9373c0" }, + new HorizontalBarChartWithAxisDataPoint { X = 35, Y = "Label 5", Legend = "Legend 5", Color = "#13a10e" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCulture.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCulture.razor new file mode 100644 index 0000000000..ecc6dba40e --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisCulture.razor @@ -0,0 +1,18 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts +@using System.Globalization + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDomainOverride.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDomainOverride.razor new file mode 100644 index 0000000000..8c589515b9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDomainOverride.razor @@ -0,0 +1,48 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + + + +@code { + private double _xMin = 0; + private double _xMax = 40000; + private double _yMin = 0; + private double _yMax = 50000; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "5000", Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "5K" }, + new HorizontalBarChartWithAxisDataPoint { X = 20000, Y = "50000", Legend = "Dogs", Color = "#e3008c", XAxisCalloutData = "20K", YAxisCalloutData = "50K" }, + new HorizontalBarChartWithAxisDataPoint { X = 25000, Y = "30000", Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "25K", YAxisCalloutData = "30K" }, + new HorizontalBarChartWithAxisDataPoint { X = 40000, Y = "13000", Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "40K", YAxisCalloutData = "13K" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisGradient.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisGradient.razor new file mode 100644 index 0000000000..187cb9faf8 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisGradient.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLabels.razor new file mode 100644 index 0000000000..437025f296 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLabels.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLegends.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLegends.razor new file mode 100644 index 0000000000..1234aff793 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideLegends.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideTooltip.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideTooltip.razor new file mode 100644 index 0000000000..bf99c8d324 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisHideTooltip.razor @@ -0,0 +1,23 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ + + + +@code { + private bool _hideTooltip = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisLegendListLabel.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisLegendListLabel.razor new file mode 100644 index 0000000000..c576907ba9 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisLegendListLabel.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisMultipleLegendSelection.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisMultipleLegendSelection.razor new file mode 100644 index 0000000000..6927126231 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisMultipleLegendSelection.razor @@ -0,0 +1,28 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ + + + +@code { + private bool _allowMultiple = true; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "Q1", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = -5000, Y = "Q1", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "-5K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = 8000, Y = "Q1", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "8K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = -7000, Y = "Q2", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "-7K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 12000, Y = "Q2", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "12K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 3000, Y = "Q2", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 15000, Y = "Q3", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "15K", YAxisCalloutData = "Q3" }, + new HorizontalBarChartWithAxisDataPoint { X = -4000, Y = "Q3", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "-4K", YAxisCalloutData = "Q3" }, + new HorizontalBarChartWithAxisDataPoint { X = 5000, Y = "Q3", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "5K", YAxisCalloutData = "Q3" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNegative.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNegative.razor new file mode 100644 index 0000000000..b8740e8213 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNegative.razor @@ -0,0 +1,56 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private static readonly string[] Categories = ["A", "B", "C", "D", "E"]; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10, Y = "A", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 20, Y = "B", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 30, Y = "C", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 40, Y = "D", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 50, Y = "E", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -10, Y = "A", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -20, Y = "B", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -30, Y = "C", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -40, Y = "D", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -50, Y = "E", Legend = "Series 1", Color = "#637cef", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 20, Y = "A", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 30, Y = "B", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 40, Y = "C", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 50, Y = "D", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 60, Y = "E", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -20, Y = "A", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -30, Y = "B", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -40, Y = "C", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -50, Y = "D", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -60, Y = "E", Legend = "Series 2", Color = "#e3008c", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 30, Y = "A", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 40, Y = "B", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 50, Y = "C", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 60, Y = "D", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 70, Y = "E", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -30, Y = "A", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -40, Y = "B", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -50, Y = "C", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -60, Y = "D", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -70, Y = "E", Legend = "Series 3", Color = "#2aa0a4", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 40, Y = "A", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 50, Y = "B", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 60, Y = "C", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 70, Y = "D", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = 80, Y = "E", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -40, Y = "A", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -50, Y = "B", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -60, Y = "C", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -70, Y = "D", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + new HorizontalBarChartWithAxisDataPoint { X = -80, Y = "E", Legend = "Series 4", Color = "#9373c0", YAxisCalloutData = "2020/04/30", XAxisCalloutData = "10%" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNumericYAxis.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNumericYAxis.razor new file mode 100644 index 0000000000..04e30c1ae8 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisNumericYAxis.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "5000", Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "5K" }, + new HorizontalBarChartWithAxisDataPoint { X = 20000, Y = "50000", Legend = "Dogs", Color = "#e3008c", XAxisCalloutData = "20K", YAxisCalloutData = "50K" }, + new HorizontalBarChartWithAxisDataPoint { X = 25000, Y = "30000", Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "25K", YAxisCalloutData = "30K" }, + new HorizontalBarChartWithAxisDataPoint { X = 40000, Y = "13000", Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "40K", YAxisCalloutData = "13K" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.razor new file mode 100644 index 0000000000..d0baac59f0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRTL.razor @@ -0,0 +1,18 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ + +
+ +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "5000", Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "5K" }, + new HorizontalBarChartWithAxisDataPoint { X = 20000, Y = "50000", Legend = "Dogs", Color = "#e3008c", XAxisCalloutData = "20K", YAxisCalloutData = "50K" }, + new HorizontalBarChartWithAxisDataPoint { X = 25000, Y = "30000", Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "25K", YAxisCalloutData = "30K" }, + new HorizontalBarChartWithAxisDataPoint { X = 40000, Y = "13000", Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "40K", YAxisCalloutData = "13K" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRoundedCorners.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRoundedCorners.razor new file mode 100644 index 0000000000..cc6a22ea06 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisRoundedCorners.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisShowYAxisLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisShowYAxisLabels.razor new file mode 100644 index 0000000000..e3bf8bbaf7 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisShowYAxisLabels.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "5000", Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "5K" }, + new HorizontalBarChartWithAxisDataPoint { X = 20000, Y = "50000", Legend = "Dogs", Color = "#e3008c", XAxisCalloutData = "20K", YAxisCalloutData = "50K" }, + new HorizontalBarChartWithAxisDataPoint { X = 25000, Y = "30000", Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "25K", YAxisCalloutData = "30K" }, + new HorizontalBarChartWithAxisDataPoint { X = 40000, Y = "13000", Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "40K", YAxisCalloutData = "13K" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSingleColor.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSingleColor.razor new file mode 100644 index 0000000000..7a551655d0 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSingleColor.razor @@ -0,0 +1,17 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStacked.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStacked.razor new file mode 100644 index 0000000000..440c8e9d41 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStacked.razor @@ -0,0 +1,21 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { X = 10000, Y = "Q1", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "10K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = -5000, Y = "Q1", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "-5K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = 8000, Y = "Q1", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "8K", YAxisCalloutData = "Q1" }, + new HorizontalBarChartWithAxisDataPoint { X = -7000, Y = "Q2", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "-7K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 12000, Y = "Q2", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "12K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 3000, Y = "Q2", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "Q2" }, + new HorizontalBarChartWithAxisDataPoint { X = 15000, Y = "Q3", Legend = "Product A", Color = "#637cef", XAxisCalloutData = "15K", YAxisCalloutData = "Q3" }, + new HorizontalBarChartWithAxisDataPoint { X = -4000, Y = "Q3", Legend = "Product B", Color = "#e3008c", XAxisCalloutData = "-4K", YAxisCalloutData = "Q3" }, + new HorizontalBarChartWithAxisDataPoint { X = 5000, Y = "Q3", Legend = "Product C", Color = "#2aa0a4", XAxisCalloutData = "5K", YAxisCalloutData = "Q3" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStringYAxis.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStringYAxis.razor new file mode 100644 index 0000000000..80c3d0c0a3 --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisStringYAxis.razor @@ -0,0 +1,16 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + + + + +@code { + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisYAxisPadding.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisYAxisPadding.razor new file mode 100644 index 0000000000..ff23ecef2f --- /dev/null +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisYAxisPadding.razor @@ -0,0 +1,26 @@ +@using Microsoft.FluentUI.AspNetCore.Components.Charts + +
+ +
+ + + + +@code { + private int _paddingTenths = 5; + private double _padding => _paddingTenths / 10.0; + + private IReadOnlyList ChartData = + [ + new HorizontalBarChartWithAxisDataPoint { Y = "String One", X = 1000, Legend = "Oranges", Color = "#637cef", XAxisCalloutData = "1K", YAxisCalloutData = "String One" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Two", X = 5000, Legend = "Grapes", Color = "#e3008c", XAxisCalloutData = "5K", YAxisCalloutData = "String Two" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Three", X = 3000, Legend = "Apples", Color = "#2aa0a4", XAxisCalloutData = "3K", YAxisCalloutData = "String Three" }, + new HorizontalBarChartWithAxisDataPoint { Y = "String Four", X = 2000, Legend = "Bananas", Color = "#9373c0", XAxisCalloutData = "2K", YAxisCalloutData = "String Four" }, + ]; +} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md index 8ba47ff28e..48bcd0a69c 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentDonutChartPage.md @@ -40,31 +40,81 @@ Donut charts are used to show proportion, which expresses a partial value in com ### Basic example +The default example renders a donut chart with color-coded segments, a center label, and a legend showing each category's data value. + {{ DonutChartDefault }} ### With labels as percentages > [!NOTE] There is no value shown in the center of the Donut Chart because the 'ValueInsideDonut' property is not set. +Setting `ShowLabelsInPercent` displays each segment's label as a percentage of the total rather than as a raw numeric value. + {{ DonutChartShowLabelsInPercent }} ### Hide labels +Setting `HideLabels` suppresses the callout labels around the chart segments, leaving only the legend to identify each slice. + {{ DonutChartHideLabels }} ### Without legends +Setting `HideLegends` removes the legend list below the chart, useful when the segment colors and callout labels provide sufficient identification. + {{ DonutChartHideLegends }} ### Rounded corners +Enabling `RoundedCorners` applies a small border-radius to each segment arc, giving the chart a softer, more modern appearance. + {{ DonutChartRoundedCorners }} ### With custom sizing +Use the sliders to adjust the chart's `Width`, `Height`, and `InnerRadius` at runtime, demonstrating how the donut ring scales with the available space. + {{ DonutChartSizing }} -### RTL Donut Chart +### Outside labels + +Enabling `ShowOutsideLabels` moves each segment's label outside the ring, avoiding overlap for charts with many small segments. + +{{ DonutChartOutsideLabels }} + +### Hide tooltip + +Setting `HideTooltip` disables the hover callout so that mousing over a segment no longer shows a data tooltip. + +{{ DonutChartHideTooltip }} + +### Legend list label + +The `LegendListLabel` property sets an accessible heading for the legend list, useful when the chart is embedded in a larger page that requires descriptive landmark text. + +{{ DonutChartLegendListLabel }} + +### Culture + +Setting the `Culture` property to a specific locale (here `de-DE`) formats all numeric values in labels and tooltips according to that culture's conventions. + +{{ DonutChartCulture }} + +### Multiple legend selection + +When `AllowMultipleLegendSelection` is enabled, clicking a legend item highlights only the corresponding segment; multiple legend items can be selected at the same time. + +{{ DonutChartMultipleLegendSelection }} + +### Value inside donut + +The `ValueInsideDonut` property sets the text displayed in the center of the ring; use the text input to change the value and see the chart update in real time. + +{{ DonutChartValueInsideDonut }} + +### RTL + +Demonstrates the right-to-left layout mode, where segment labels and the legend are mirrored for RTL language support. {{ DonutChartDefaultRTL }} diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md index 11174e0c32..1d58f55c20 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md @@ -50,26 +50,110 @@ Horizontal bar chart can be of 2 types - ### Default horizontal bar chart +The default variant renders multiple series, each as a stacked bar composed of several color-coded segments. The chart automatically distributes the data across the available width. + {{ HorizontalBarChartDefault }} ### Single Bar +The single-bar variant displays one bar per series with a single data segment, making it easy to compare a single value across multiple categories at a glance. + {{ HorizontalBarChartSingleBar }} ### Single Bar NM Variant +The n/M variant shows each bar scaled relative to its own target value rather than a shared maximum, allowing per-category comparison against individual goals. + {{ HorizontalBarChartSingleBarNMVariant }} ### Benchmark +The benchmark example adds an inverted-triangle indicator to the bar, visually marking a reference or target value within each series. + {{ HorizontalBarChartBenchmark }} ### Single Data Point +This example shows how the chart renders when each series contains only a single data point, useful for simple value comparisons without stacking. + {{ HorizontalBarChartSingleDataPoint }} +### Hide labels + +Setting `HideLabels` removes the numeric value labels shown at the end of each bar, producing a cleaner visual when precise values are not required. + +{{ HorizontalBarChartHideLabels }} + +### Hide ratio + +When `HideRatio` is set, the fractional value shown to the right of the bar (e.g. 1543/15000) is suppressed, leaving only the bar itself. + +{{ HorizontalBarChartHideRatio }} + +### Hide legends + +Setting `HideLegends` removes the legend list below the chart, useful when the bar titles alone provide sufficient context. + +{{ HorizontalBarChartHideLegends }} + +### Hide tooltip + +Setting `HideTooltip` disables the hover tooltip so that hovering over a bar segment no longer shows a callout with the data value. + +{{ HorizontalBarChartHideTooltip }} + +### Rounded corners + +Enabling `RoundedCorners` applies a small border-radius to each bar segment, giving the chart a softer, more modern appearance. + +{{ HorizontalBarChartRoundedCorners }} + +### Gradient + +Enabling `EnableGradient` fills each bar segment with a gradient that transitions from a lighter tint to the segment's full color. + +{{ HorizontalBarChartGradient }} + +### Fraction mode + +Setting `ChartDataMode` to `fraction` displays the value label as a numerator/denominator pair (e.g. 1543/15000) so the viewer can see both the current value and the total at a glance. + +{{ HorizontalBarChartChartDataModeFraction }} + +### Percentage mode + +Setting `ChartDataMode` to `percentage` replaces the raw numeric label with a rounded percentage of the total, giving a proportional view of each bar's value. + +{{ HorizontalBarChartChartDataModePercentage }} + +### Data mode (interactive) + +Use the selector to switch between `default`, `fraction`, and `percentage` display modes at runtime, illustrating how `ChartDataMode` changes the value label without altering the bar lengths. + +{{ HorizontalBarChartChartDataModeInteractive }} + +### Legend list label + +The `LegendListLabel` property sets an accessible heading for the legend list, useful when the chart is embedded in a larger page that requires descriptive landmark text. + +{{ HorizontalBarChartLegendListLabel }} + +### Culture + +Setting the `Culture` property to a specific locale (here `de-DE`) formats all numeric values and separators according to that culture's conventions. + +{{ HorizontalBarChartCulture }} + +### Multiple legend selection + +When `AllowMultipleLegendSelection` is enabled, clicking a legend item highlights only the corresponding bar segments; multiple items can be selected simultaneously. + +{{ HorizontalBarChartMultipleLegendSelection }} + ### RTL +Demonstrates the right-to-left layout mode, where bars grow from the right edge and all labels are mirrored for RTL language support. + {{ HorizontalBarChartDefaultRTL }} ## API Fluent Horizontal Bar Chart diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md index 320b190e6d..ea721a86b7 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md @@ -7,4 +7,136 @@ route: /Charts/HorizontalBarChartWithAxis A horizontal bar chart with axis is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent, along with an axis to provide context for the values. This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long, while also providing a clear reference point for the values being compared. +### Default + +The default example renders a grouped horizontal bar chart with a numeric x-axis and string y-axis labels, showing the baseline appearance of the component. + {{ HorizontalBarChartWithAxisDefault }} + +### String Y-axis + +Demonstrates using string labels on the y-axis, where each category is identified by a text value rather than a numeric position. + +{{ HorizontalBarChartWithAxisStringYAxis }} + +### Numeric Y-axis + +Demonstrates using numeric values on the y-axis, suitable for scenarios where categories are identified by ordered numeric keys. + +{{ HorizontalBarChartWithAxisNumericYAxis }} + +### Stacked bars + +In the stacked variant, multiple data series are layered into a single bar per category, making it easy to see both part-to-whole relationships and totals. + +{{ HorizontalBarChartWithAxisStacked }} + +### Negative values + +Shows how the chart handles data that contains negative values, with bars extending in both directions from the zero baseline. + +{{ HorizontalBarChartWithAxisNegative }} + +### Category order + +Use the selector to change the ordering of the y-axis categories at runtime, illustrating how the `CategoryOrder` property rearranges bars without changing the underlying data. + +{{ HorizontalBarChartWithAxisCategoryOrder }} + +### Single color + +Setting a single color for all series removes per-series color differentiation, useful when the category labels alone carry the semantic meaning. + +{{ HorizontalBarChartWithAxisSingleColor }} + +### Hide legends + +Setting `HideLegends` removes the legend list below the chart, reducing visual clutter when series are already identifiable from the axis labels. + +{{ HorizontalBarChartWithAxisHideLegends }} + +### Gradient + +Enabling `EnableGradient` fills each bar with a gradient that transitions from a lighter tint at the start to the full series color at the end. + +{{ HorizontalBarChartWithAxisGradient }} + +### Rounded corners + +Enabling `RoundedCorners` applies a small border-radius to each bar, giving a softer appearance while retaining the same data layout. + +{{ HorizontalBarChartWithAxisRoundedCorners }} + +### Show Y-axis labels + +Enabling `ShowYAxisLabels` adds text labels directly on the y-axis ticks, making category names visible without requiring the legend. + +{{ HorizontalBarChartWithAxisShowYAxisLabels }} + +### Culture + +Setting the `Culture` property to a specific locale (here `de-DE`) formats all numeric axis tick values and tooltips according to that culture's conventions. + +{{ HorizontalBarChartWithAxisCulture }} + +### Legend list label + +The `LegendListLabel` property sets an accessible heading for the legend list, useful when the chart is embedded in a larger page that requires descriptive landmark text. + +{{ HorizontalBarChartWithAxisLegendListLabel }} + +### Hide tooltip + +Setting `HideTooltip` disables the hover callout so that mousing over a bar no longer shows a data tooltip. + +{{ HorizontalBarChartWithAxisHideTooltip }} + +### Multiple legend selection + +When `AllowMultipleLegendSelection` is enabled, clicking a legend item highlights only the corresponding bars; multiple items can be selected at the same time. + +{{ HorizontalBarChartWithAxisMultipleLegendSelection }} + +### Hide labels + +Setting `HideLabels` suppresses the numeric value labels rendered at the end of each bar, producing a cleaner look when exact values are shown elsewhere. + +{{ HorizontalBarChartWithAxisHideLabels }} + +### Bar height + +Use the slider to adjust the `BarHeight` property at runtime, controlling the pixel thickness of each individual bar in the chart. + +{{ HorizontalBarChartWithAxisBarHeight }} + +### Axis tick counts + +Use the sliders to change the number of ticks rendered on the x-axis and y-axis independently, allowing fine-grained control over axis density. + +{{ HorizontalBarChartWithAxisAxisTickCounts }} + +### Y-axis padding + +Use the slider to adjust the `YAxisPadding` property, which controls the proportional gap between the y-axis labels and the start of the bars. + +{{ HorizontalBarChartWithAxisYAxisPadding }} + +### Domain override + +Use the sliders to override the minimum and maximum values of both axes, demonstrating how `XMinValue`, `XMaxValue`, `YMinValue`, and `YMaxValue` can constrain or expand the visible data range. + +{{ HorizontalBarChartWithAxisDomainOverride }} + +### RTL + +Demonstrates the right-to-left layout mode, where bars grow from the right edge and axis labels are mirrored to support RTL languages. + +{{ HorizontalBarChartWithAxisRTL }} + +## API Fluent Horizontal Bar Chart With Axis + +{{ API Type=FluentHorizontalBarChartWithAxis }} + +## API Horizontal Bar Chart With Axis Data Point + +{{ API Type=HorizontalBarChartWithAxisDataPoint Properties=All }} diff --git a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts index 35e480cbad..34414b8f6b 100644 --- a/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts +++ b/src/Charts.Scripts/src/horizontal-bar-chart-with-axis/horizontal-bar-chart-with-axis.ts @@ -684,7 +684,8 @@ export class HorizontalBarChartWithAxis extends FASTElement { }); this.legends = Array.from(legendColorMap.entries()).map(([legend, color]) => ({ legend, color })); - this.chartContainer.appendChild(svg); this._updateLegendInteractionState(); + this.chartContainer.appendChild(svg); + this._updateLegendInteractionState(); } private _clearChart() { diff --git a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor index 59b1e5be13..550eae75cd 100644 --- a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor +++ b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor @@ -8,9 +8,15 @@ data="@ChartJson.Serialize(ChartData!)" variant="@(Variant is null ? null : Variant.Value.ToAttributeValue())" hide-ratio="@(HideRatio ? "true" : null)" + hide-labels="@(HideLabels ? "true" : null)" hide-legends="@(HideLegends ? "true" : null)" hide-tooltip="@(HideTooltip ? "true" : null)" + round-corners="@(RoundedCorners ? "true" : null)" + enable-gradient="@(EnableGradient ? "true" : null)" + allow-multiple-legend-selection="@(AllowMultipleLegendSelection ? "true" : null)" legend-list-label="@LegendListLabel" chart-title="@ChartTitle" + chart-data-mode="@ChartDataMode" + culture="@Culture.Name" @attributes="@AdditionalAttributes"> diff --git a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs index c841a42220..b78444301f 100644 --- a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs +++ b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor.cs @@ -2,6 +2,7 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ +using System.Globalization; using Microsoft.AspNetCore.Components; using Microsoft.FluentUI.AspNetCore.Components.Enums; using Microsoft.FluentUI.AspNetCore.Components.Utilities; @@ -75,6 +76,42 @@ public FluentHorizontalBarChart(LibraryConfiguration configuration) : base(confi [Parameter] public string? ChartTitle { get; set; } + /// + /// Gets or sets a value indicating whether bar labels are hidden. + /// + [Parameter] + public bool HideLabels { get; set; } + + /// + /// Gets or sets a value indicating whether bars are rendered with rounded corners. + /// + [Parameter] + public bool RoundedCorners { get; set; } + + /// + /// Gets or sets a value indicating whether a gradient fill is applied to the bars. + /// + [Parameter] + public bool EnableGradient { get; set; } + + /// + /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. + /// + [Parameter] + public bool AllowMultipleLegendSelection { get; set; } + + /// + /// Gets or sets the culture used for number and date formatting in the chart. + /// + [Parameter] + public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; + + /// + /// Gets or sets the chart data mode. Accepted values are "default", "fraction", and "percentage". + /// + [Parameter] + public string? ChartDataMode { get; set; } + /// /// Gets a value indicating whether the component has data that can be rendered safely. /// diff --git a/src/Charts/Charts/HorizontalBarChart/HorizontalBarChartModels.cs b/src/Charts/Charts/HorizontalBarChart/HorizontalBarChartModels.cs new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/Charts/Models/HorizontalBarChartModels.cs b/src/Charts/Models/HorizontalBarChartModels.cs index 98b7e34a28..b21ff8be05 100644 --- a/src/Charts/Models/HorizontalBarChartModels.cs +++ b/src/Charts/Models/HorizontalBarChartModels.cs @@ -29,6 +29,7 @@ public sealed record HorizontalBarChartDataPoint /// Gets the optional total bar length used for ratio-style rendering. ///
[JsonPropertyName("total")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public double? Total { get; init; } /// From 2d514d493043d0160a541d1a46a9642018ac4ec3 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 17:56:03 +0200 Subject: [PATCH 15/60] - Introduce FluentChartBase - Update docs --- .../Components/Charts/FluentUIChartsPage.md | 9 ++- .../Charts/DonutChart/FluentDonutChart.razor | 2 +- .../DonutChart/FluentDonutChart.razor.cs | 57 +------------ src/Charts/Charts/FluentChartBase.cs | 79 +++++++++++++++++++ .../FluentHorizontalBarChart.razor | 2 +- .../FluentHorizontalBarChart.razor.cs | 59 +------------- .../FluentHorizontalBarChartWithAxis.razor | 2 +- .../FluentHorizontalBarChartWithAxis.razor.cs | 62 +-------------- 8 files changed, 93 insertions(+), 179 deletions(-) create mode 100644 src/Charts/Charts/FluentChartBase.cs diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md index 68a617f275..aa0c5f3a42 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md @@ -6,10 +6,17 @@ icon: ChartMultiple # Charts -The Fluent UI Charts are a set of components that allow you to easily create charts in your Blazor applications. +The Fluent UI Charts are a set of Razor components that allow you to easily use charts in your Blazor applications. In the future, the package will +be extended with more chart types (based on the Fluent UI React v9 Charts package). Currently, the following chart types are available: - [Donut Chart](/Charts/DonutChart) - [Horizontal Bar Chart](/Charts/HorizontalBarChart) - [Horizontal Bar Chart with Axis](/Charts/HorizontalBarChartWithAxis) + +## API Fluent Chart Base + +All charts in the Fluent UI Charts package inherit from the `FluentChartBase` component, which provides common parameters for all chart types. + +{{ API Type="FluentChartBase" }} diff --git a/src/Charts/Charts/DonutChart/FluentDonutChart.razor b/src/Charts/Charts/DonutChart/FluentDonutChart.razor index 66e51a9b0b..3e5d1b4b29 100644 --- a/src/Charts/Charts/DonutChart/FluentDonutChart.razor +++ b/src/Charts/Charts/DonutChart/FluentDonutChart.razor @@ -1,5 +1,5 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts -@inherits FluentComponentBase +@inherits FluentChartBase /// A FluentDonutChart is a component that displays data in a donut chart format. /// -public partial class FluentDonutChart : FluentComponentBase +public partial class FluentDonutChart : FluentChartBase { /// public FluentDonutChart(LibraryConfiguration configuration) : base(configuration) { - Id = Identifier.NewId(); } /// @@ -31,12 +28,6 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration .AddStyle("height", Height.HasValue ? $"{Height.Value}px" : null, when: Height.HasValue) .Build(); - /// - /// Gets or sets the title of the donut chart, which is typically displayed above the chart to provide context about the data being represented. - /// - [Parameter] - public string ChartTitle { get; set; } = string.Empty; - /// /// Gets or sets the data for the donut chart. /// @@ -55,30 +46,6 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration [Parameter] public int? Width { get; set; } - /// - /// Gets or sets a value indicating whether labels are hidden in the component output. Default is false. - /// - [Parameter] - public bool HideLabels { get; set; } - - /// - /// Gets or sets a value indicating whether legends are hidden in the component output. - /// - [Parameter] - public bool HideLegends { get; set; } - - /// - /// Gets or sets a value indicating whether the tooltip is hidden. - /// - [Parameter] - public bool HideTooltip { get; set; } - - /// - /// Gets or sets whether bars/arcs in the chart should have rounded corners. - /// - [Parameter] - public bool RoundedCorners { get; set; } - /// /// Gets or sets whether label values should be displayed as percentages of the total rather than raw values. /// @@ -99,26 +66,4 @@ public FluentDonutChart(LibraryConfiguration configuration) : base(configuration /// [Parameter] public string? ValueInsideDonut { get; set; } - - /// - /// Gets or sets the label text displayed for the legend list. - /// - [Parameter] - public string? LegendListLabel { get; set; } - - /// - /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. - /// When , clicking a legend item adds it to the active selection rather than replacing the current selection. - /// When (default), only a single legend item can be selected at a time. - /// - [Parameter] - public bool AllowMultipleLegendSelection { get; set; } - - /// - /// Gets or sets the culture used for locale-aware number formatting of values in the chart - /// (labels, tooltips, and inner donut text). - /// By default to display using the OS culture. - /// - [Parameter] - public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; } diff --git a/src/Charts/Charts/FluentChartBase.cs b/src/Charts/Charts/FluentChartBase.cs new file mode 100644 index 0000000000..7bd7f1387d --- /dev/null +++ b/src/Charts/Charts/FluentChartBase.cs @@ -0,0 +1,79 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using Microsoft.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Utilities; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Abstract base class for FluentUI chart components. +/// Provides parameters that are common across all chart types. +/// +public abstract partial class FluentChartBase : FluentComponentBase +{ + /// + protected FluentChartBase(LibraryConfiguration configuration) : base(configuration) + { + Id = Identifier.NewId(); + } + + /// + /// Gets or sets the title text displayed on the chart. + /// + [Parameter] + public string? ChartTitle { get; set; } + + /// + /// Gets or sets a value indicating whether legends are hidden in the component output. + /// + [Parameter] + public bool HideLegends { get; set; } + + /// + /// Gets or sets a value indicating whether the tooltip is hidden. + /// + [Parameter] + public bool HideTooltip { get; set; } + + /// + /// Gets or sets a value indicating whether bar or arc labels are hidden. + /// + [Parameter] + public bool HideLabels { get; set; } + + /// + /// Gets or sets the label displayed for the legend list. + /// + [Parameter] + public string? LegendListLabel { get; set; } + + /// + /// Gets or sets a value indicating whether bars or arcs are rendered with rounded corners. + /// + [Parameter] + public bool RoundedCorners { get; set; } + + /// + /// Gets or sets a value indicating whether a gradient fill is applied to the bars or arcs. + /// + [Parameter] + public bool EnableGradient { get; set; } + + /// + /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. + /// When , clicking a legend item adds it to the active selection rather than replacing the current selection. + /// When (default), only a single legend item can be selected at a time. + /// + [Parameter] + public bool AllowMultipleLegendSelection { get; set; } + + /// + /// Gets or sets the culture used for locale-aware number formatting of values in the chart. + /// Defaults to to display using the OS culture. + /// + [Parameter] + public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; +} diff --git a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor index 550eae75cd..1250040f74 100644 --- a/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor +++ b/src/Charts/Charts/HorizontalBarChart/FluentHorizontalBarChart.razor @@ -1,6 +1,6 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts @using Microsoft.FluentUI.AspNetCore.Components.Extensions -@inherits FluentComponentBase +@inherits FluentChartBase /// A FluentHorizontalBarChart is a component that displays data in a horizontal bar chart format. /// -public partial class FluentHorizontalBarChart : FluentComponentBase +public partial class FluentHorizontalBarChart : FluentChartBase { /// public FluentHorizontalBarChart(LibraryConfiguration configuration) : base(configuration) { - Id = Identifier.NewId(); } /// @@ -52,60 +49,6 @@ public FluentHorizontalBarChart(LibraryConfiguration configuration) : base(confi [Parameter] public bool HideRatio { get; set; } - /// - /// Gets or sets a value indicating whether legends are hidden in the component output. - /// - [Parameter] - public bool HideLegends { get; set; } - - /// - /// Gets or sets a value indicating whether the tooltip is hidden. - /// - [Parameter] - public bool HideTooltip { get; set; } - - /// - /// Gets or sets the label displayed for the legend list. - /// - [Parameter] - public string? LegendListLabel { get; set; } - - /// - /// Gets or sets the title text displayed on the chart. - /// - [Parameter] - public string? ChartTitle { get; set; } - - /// - /// Gets or sets a value indicating whether bar labels are hidden. - /// - [Parameter] - public bool HideLabels { get; set; } - - /// - /// Gets or sets a value indicating whether bars are rendered with rounded corners. - /// - [Parameter] - public bool RoundedCorners { get; set; } - - /// - /// Gets or sets a value indicating whether a gradient fill is applied to the bars. - /// - [Parameter] - public bool EnableGradient { get; set; } - - /// - /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. - /// - [Parameter] - public bool AllowMultipleLegendSelection { get; set; } - - /// - /// Gets or sets the culture used for number and date formatting in the chart. - /// - [Parameter] - public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; - /// /// Gets or sets the chart data mode. Accepted values are "default", "fraction", and "percentage". /// diff --git a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor index 9ccccf00e7..2398d62c00 100644 --- a/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor +++ b/src/Charts/Charts/HorizontalBarChartWithAxis/FluentHorizontalBarChartWithAxis.razor @@ -1,6 +1,6 @@ @namespace Microsoft.FluentUI.AspNetCore.Components.Charts @using Microsoft.FluentUI.AspNetCore.Components.Extensions -@inherits FluentComponentBase +@inherits FluentChartBase /// A FluentHorizontalBarChartWithAxis is a component that displays data in a horizontal bar chart format with an axis. /// -public partial class FluentHorizontalBarChartWithAxis : FluentComponentBase +public partial class FluentHorizontalBarChartWithAxis : FluentChartBase { /// public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : base(configuration) { - Id = Identifier.NewId(); } /// @@ -35,12 +32,6 @@ public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : ba [Parameter, EditorRequired] public IReadOnlyList ChartData { get; set; } = []; - /// - /// Gets or sets the title text displayed on the chart. - /// - [Parameter] - public string? ChartTitle { get; set; } - /// /// Gets or sets the height of the horizontal bar chart. /// @@ -53,56 +44,12 @@ public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : ba [Parameter] public int? Width { get; set; } - /// - /// Gets or sets a value indicating whether legends are hidden in the component output. - /// - [Parameter] - public bool HideLegends { get; set; } - - /// - /// Gets or sets a value indicating whether the tooltip is hidden. - /// - [Parameter] - public bool HideTooltip { get; set; } - - /// - /// Gets or sets a value indicating whether the labels are hidden. - /// - [Parameter] - public bool HideLabels { get; set; } - - /// - /// Gets or sets the label displayed for the legend list. - /// - [Parameter] - public string? LegendListLabel { get; set; } - /// /// Gets or sets whether to use a single color for all bars in the chart. /// [Parameter] public bool UseSingleColor { get; set; } - /// - /// Gets or sets whether gradient rendering is enabled. - /// - [Parameter] - public bool EnableGradient { get; set; } - - /// - /// Gets or sets whether the bars in the chart should have rounded corners. - /// - [Parameter] - public bool RoundedCorners { get; set; } - - /// - /// Gets or sets a value indicating whether multiple legend items can be selected simultaneously. - /// When , clicking a legend item adds it to the active selection rather than replacing the current selection. - /// When (default), only a single legend item can be selected at a time. - /// - [Parameter] - public bool AllowMultipleLegendSelection { get; set; } - /// /// Gets or sets a value indicating whether the full Y-axis labels are shown. /// When (default), long labels are truncated. @@ -174,11 +121,4 @@ public FluentHorizontalBarChartWithAxis(LibraryConfiguration configuration) : ba /// [Parameter] public double? YMaxValue { get; set; } - - /// - /// Gets or sets the culture used for locale-aware number formatting of values in the chart. - /// Defaults to to display using the OS culture. - /// - [Parameter] - public CultureInfo Culture { get; set; } = CultureInfo.CurrentCulture; } From e3a5e063e5d090d6412e5f4689ef65f01471c257 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 18:16:03 +0200 Subject: [PATCH 16/60] Use charts comments for docs --- .../Documentation/Components/Charts/FluentUIChartsPage.md | 2 -- .../Infrastructure/ServiceCollectionExtensions.cs | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md index aa0c5f3a42..c8d84740be 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/FluentUIChartsPage.md @@ -18,5 +18,3 @@ Currently, the following chart types are available: ## API Fluent Chart Base All charts in the Fluent UI Charts package inherit from the `FluentChartBase` component, which provides common parameters for all chart types. - -{{ API Type="FluentChartBase" }} diff --git a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs index c776bf0a1f..1cb6a15018 100644 --- a/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs +++ b/examples/Demo/FluentUI.Demo.Client/Infrastructure/ServiceCollectionExtensions.cs @@ -64,7 +64,10 @@ public static DemoServices AddFluentUIDemoServices(this IServiceCollection servi options.PageTitle = "{0} - FluentUI Blazor Components"; options.ComponentsAssembly = typeof(Client._Imports).Assembly; options.ResourcesAssembly = typeof(Client._Imports).Assembly; - options.ApiAssemblies = [typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly]; + options.ApiAssemblies = [ + typeof(Microsoft.FluentUI.AspNetCore.Components._Imports).Assembly, + typeof(Microsoft.FluentUI.AspNetCore.Components.Charts._Imports).Assembly + ]; options.ApiCommentSummary = (data, component, member) => { if (member is null && (data is null || data?.Items?.Count <= 1)) From d6b9bebb3865ca05e0c35e119481c086505e9f4f Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Mon, 11 May 2026 20:59:09 +0200 Subject: [PATCH 17/60] Update docs --- .../Pages/FluentHorizontalBarChartPage.md | 3 +- .../Pages/FluentHorizontalBarChartWithAxis.md | 42 ++++++++++++++++++- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md index 1d58f55c20..00d8f5eda2 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartPage.md @@ -5,7 +5,8 @@ route: /Charts/HorizontalBarChart # Horizontal Bar Chart -A horizontal bar chart is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent. This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long. +A horizontal bar chart is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent. +This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long. ## Layout diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md index ea721a86b7..633cd8f588 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Pages/FluentHorizontalBarChartWithAxis.md @@ -5,7 +5,47 @@ route: /Charts/HorizontalBarChartWithAxis # Horizontal Bar Chart With Axis -A horizontal bar chart with axis is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent, along with an axis to provide context for the values. This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long, while also providing a clear reference point for the values being compared. +A horizontal bar chart is a chart that presents categorical data with rectangular bars with lengths proportional to the values they represent. +This type of chart is particularly useful when the intention is to show comparisons among various categories and the labels for those categories are long. + +Horizontal bar chart with axis is a version of horizontal bar chart that has the x and y axis present. This chart is same as the vertical bar chart except +that the bars are aligned horizontally. + +## Layout + +The default bar height is 16px. For dense data, it can be as thin as 8px high. Always consider the visual weight of the bars in relationship to the rest +of the app before choosing this type of chart. + +The padding around the bar chart is a default of 8px from the x and y-axis container. This gives enough room for additional content like label values to +display properly without overlapping on to the Y-axis ticks. A 2:1 spacing is maintained between all the bars in the graph so that space between two bars +is always two times the bar height. This helps to ensure that the graph is not overpowering other data visualizations. For charts that display monetary values, the dollar symbol should be displayed as part of the total value. Also call out the currency in the chart title to provide additional context. Chart title can be used to communicate currency when the total labels are hidden. + +The chart can accommodate unusually long labels by shrinking the bars without distorting the visual layout. + +## Content + +- **Bar segment** Bar segments make up a bar chart. Standard size options are: 8px, 16px, and 24px with 16px being the default. +- **Value labels** (Optional) - Off by default with the option to toggle on in case the data visualization needs to communicate label values to users. + +## Accessibility + +- Bar graphs should be flexible to their containers. They will change width and height to fit their environment. + +- Type truncation should happen when the total value exceeds one thousand including 1 decimal place for the hundreds. +- For example, display full value for 600, 983, or 19.53. Truncate 6,000 to 6.0K, 9,801 to 9.8K, and 100,900 to 100.9K. + +- All the bars of the graph are accessible by screen readers and keyboard navigation using Up and Down arrow keys or Tab. + +## Do's + +- Try to keep the number of bars in the chart between 3 and 20 to maximize readability. +- Use this chart if the bar labels are very long. + +## Dont's + +- Don't keep the bar values in random order. Horizontal bar chart is most effective if the bars are sorted in either ascending or descending order. + +## Examples ### Default From e79a22a292f3b7c969cade25ca904f5ddd9a2477 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 10:45:26 +0200 Subject: [PATCH 18/60] Update package-lock --- src/Charts.Scripts/package-lock.json | 1065 +++++++++++++++++++++++--- 1 file changed, 945 insertions(+), 120 deletions(-) diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json index bf6505e3f1..53826d5b9c 100644 --- a/src/Charts.Scripts/package-lock.json +++ b/src/Charts.Scripts/package-lock.json @@ -8,7 +8,7 @@ "name": "microsoft.fluentui.aspnetcore.components.charts.assets", "license": "ISC", "dependencies": { - "@fluentui/web-components": "^3.0.0-rc.13", + "@fluentui/web-components": "^3.0.0-rc.18", "@microsoft/fast-web-utilities": "^6.0.0", "d3-format": "^3.0.0", "d3-selection": "^3.0.0", @@ -19,16 +19,17 @@ "@types/d3-format": "^3.0.0", "@types/d3-selection": "^3.0.0", "@types/d3-shape": "^3.0.0", - "esbuild": "0.27.4", + "esbuild": "0.28.0", "esbuild-plugin-inline-css": "0.0.1", + "eslint": "10.3.0", "glob": "^13.0.6", "rimraf": "6.1.3" } }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha1-eiicFY4py/WeoK/IPMgPBtHIlAI=", "cpu": [ "ppc64" ], @@ -43,9 +44,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha1-XsGEdgXgW12+XfkNuf9+PkxY3Kc=", "cpu": [ "arm" ], @@ -60,9 +61,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha1-uIKNnt+jqSZgZE643m5PPCA9exc=", "cpu": [ "arm64" ], @@ -77,9 +78,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha1-OQZCF1uI74K61MzgP4qxP+mxkS4=", "cpu": [ "x64" ], @@ -94,9 +95,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha1-rkUyWWDVlQzWlR5PlzlvTh/32NM=", "cpu": [ "arm64" ], @@ -111,9 +112,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha1-wHkkfViba5lEllnZTwaVG4S/8uQ=", "cpu": [ "x64" ], @@ -128,9 +129,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha1-RcRWIVpIZZPJSQApcgLcEciAo3o=", "cpu": [ "arm64" ], @@ -145,9 +146,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha1-A5lJTByF5DiOm3BAvWDUjypbDSw=", "cpu": [ "x64" ], @@ -162,9 +163,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha1-e0L/qEwoiulP3EMcGyionjw7kng=", "cpu": [ "arm" ], @@ -179,9 +180,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha1-1tnwnvDeVBFr9Fmk1TysfglS/jk=", "cpu": [ "arm64" ], @@ -196,9 +197,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha1-3rFdES7Y3WBTRra5U9I6If+BJT8=", "cpu": [ "ia32" ], @@ -213,9 +214,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha1-gfuJ0H7sx5sVfephAzdXcm/ODKQ=", "cpu": [ "loong64" ], @@ -230,9 +231,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha1-0OQmkbP/evn7Ihe3D8AfNDvbYrs=", "cpu": [ "mips64el" ], @@ -247,9 +248,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha1-OJ8+XpjxfUd8RnzIcTbhoHburYc=", "cpu": [ "ppc64" ], @@ -264,9 +265,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha1-djvWDVmyQr4S2h5n1XKfMCTGBfo=", "cpu": [ "riscv64" ], @@ -281,9 +282,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha1-qsYGFjSHLkZ33mk7zoAw1zsf0FU=", "cpu": [ "s390x" ], @@ -298,9 +299,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha1-TykXdHGI/ndjK87GWy2EtCJBl3k=", "cpu": [ "x64" ], @@ -315,9 +316,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha1-gU3wrlegw4aBRJG4OX7rqCCUqUc=", "cpu": [ "arm64" ], @@ -332,9 +333,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha1-4BvffmD6GgjkbUbZYLDZu4rCEK8=", "cpu": [ "x64" ], @@ -349,9 +350,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha1-ShXDaqzKaNLVpMkLcQwGdZ9MH/o=", "cpu": [ "arm64" ], @@ -366,9 +367,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha1-R15hAUmKjszjAI18OIER16J8F70=", "cpu": [ "x64" ], @@ -383,9 +384,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha1-z9w5V/C3pp8b3hKarRf8wvb6Az4=", "cpu": [ "arm64" ], @@ -400,9 +401,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha1-oBPIVv7KzRw67Jhciv4dHLAXSX0=", "cpu": [ "x64" ], @@ -417,9 +418,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha1-6uBeDzUnHK04mLQxaNPpo7uvR+U=", "cpu": [ "arm64" ], @@ -434,9 +435,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha1-BhYevFv3XAjWn+s8ayJWBRWROZg=", "cpu": [ "ia32" ], @@ -451,9 +452,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "http://localhost:4873/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", + "integrity": "sha1-BNkNV1K0zmXStqwl66CP92JP4Hw=", "cpu": [ "x64" ], @@ -467,6 +468,113 @@ "node": ">=18" } }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha1-TpCvZ7xR3e5s3vUoTt9XLsN2tZU=", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.23.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-array/-/config-array-0.23.5.tgz", + "integrity": "sha1-VuhtJDBJGV2KzAwGobPf3D+j3pU=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^3.0.5", + "debug": "^4.3.1", + "minimatch": "^10.2.4" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.5.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", + "integrity": "sha1-rhYTTkeSrF+9xTNUiiSsHqn3864=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/core": { + "version": "1.2.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/core/-/core-1.2.1.tgz", + "integrity": "sha1-wdp80bgvqHh/mLVin7gRhIobY84=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/object-schema": { + "version": "3.0.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/object-schema/-/object-schema-3.0.5.tgz", + "integrity": "sha1-iOm/TRHSsZwILnjr586IckpesJE=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.7.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", + "integrity": "sha1-xBJf0BXs7rCbeTEJ/bzU3QoC00Y=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^1.2.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + } + }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", "resolved": "http://localhost:4873/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", @@ -477,21 +585,86 @@ } }, "node_modules/@fluentui/web-components": { - "version": "3.0.0-rc.13", - "resolved": "http://localhost:4873/@fluentui/web-components/-/web-components-3.0.0-rc.13.tgz", - "integrity": "sha1-ATuXKxB4p/FDESanYDaRZzwnI9w=", + "version": "3.0.0-rc.18", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/web-components/-/web-components-3.0.0-rc.18.tgz", + "integrity": "sha1-9G1wtr6T19+qSkZrC82xwMEsbvg=", "license": "MIT", "dependencies": { "@fluentui/tokens": "1.0.0-alpha.23", - "@microsoft/fast-web-utilities": "^6.0.0", - "tabbable": "^6.2.0", "tslib": "^2.1.0" }, "engines": { "node": "^22.0.0 || ^24.0.0" }, "peerDependencies": { - "@microsoft/fast-element": "^2.0.0" + "@microsoft/fast-element": "^2.0.0", + "@microsoft/focusgroup-polyfill": "^1.4.1" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, "node_modules/@microsoft/fast-element": { @@ -509,6 +682,13 @@ "exenv-es6": "^1.1.1" } }, + "node_modules/@microsoft/focusgroup-polyfill": { + "version": "1.4.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/focusgroup-polyfill/-/focusgroup-polyfill-1.4.1.tgz", + "integrity": "sha1-DT/u5nV3X3ppKjsel07qEHUDx44=", + "license": "MIT", + "peer": true + }, "node_modules/@swc/helpers": { "version": "0.5.21", "resolved": "http://localhost:4873/@swc/helpers/-/helpers-0.5.21.tgz", @@ -549,6 +729,67 @@ "@types/d3-path": "*" } }, + "node_modules/@types/esrecurse": { + "version": "4.3.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/esrecurse/-/esrecurse-4.3.1.tgz", + "integrity": "sha1-b2Nq+WL75hkbgwvWdrpZhpJrzOw=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", + "dev": true, + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha1-TOecib5Ar+ev6POtuQKh8c6awIo=", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "http://localhost:4873/balanced-match/-/balanced-match-4.0.4.tgz", @@ -584,6 +825,21 @@ "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==", "dev": true }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, "node_modules/d3-format": { "version": "3.1.2", "resolved": "http://localhost:4873/d3-format/-/d3-format-3.1.2.tgz", @@ -623,10 +879,35 @@ "node": ">=12" } }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/debug/-/debug-4.4.3.tgz", + "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", + "dev": true, + "license": "MIT" + }, "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "http://localhost:4873/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esbuild/-/esbuild-0.28.0.tgz", + "integrity": "sha1-Xe40f/s+OHQhKjWmmDawd7HObZY=", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -637,32 +918,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" + "@esbuild/aix-ppc64": "0.28.0", + "@esbuild/android-arm": "0.28.0", + "@esbuild/android-arm64": "0.28.0", + "@esbuild/android-x64": "0.28.0", + "@esbuild/darwin-arm64": "0.28.0", + "@esbuild/darwin-x64": "0.28.0", + "@esbuild/freebsd-arm64": "0.28.0", + "@esbuild/freebsd-x64": "0.28.0", + "@esbuild/linux-arm": "0.28.0", + "@esbuild/linux-arm64": "0.28.0", + "@esbuild/linux-ia32": "0.28.0", + "@esbuild/linux-loong64": "0.28.0", + "@esbuild/linux-mips64el": "0.28.0", + "@esbuild/linux-ppc64": "0.28.0", + "@esbuild/linux-riscv64": "0.28.0", + "@esbuild/linux-s390x": "0.28.0", + "@esbuild/linux-x64": "0.28.0", + "@esbuild/netbsd-arm64": "0.28.0", + "@esbuild/netbsd-x64": "0.28.0", + "@esbuild/openbsd-arm64": "0.28.0", + "@esbuild/openbsd-x64": "0.28.0", + "@esbuild/openharmony-arm64": "0.28.0", + "@esbuild/sunos-x64": "0.28.0", + "@esbuild/win32-arm64": "0.28.0", + "@esbuild/win32-ia32": "0.28.0", + "@esbuild/win32-x64": "0.28.0" } }, "node_modules/esbuild-plugin-inline-css": { @@ -677,12 +958,249 @@ "sha256": "^0.2.0" } }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "10.3.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint/-/eslint-10.3.0.tgz", + "integrity": "sha1-7VuBDrjgGRvyS93PnNtFuXTgoW0=", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.2", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.5.5", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^9.1.2", + "eslint-visitor-keys": "^5.0.1", + "espree": "^11.2.0", + "esquery": "^1.7.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "minimatch": "^10.2.4", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "9.1.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-scope/-/eslint-scope-9.1.2.tgz", + "integrity": "sha1-ud5qzi+rHP8k0uWNhbdMj86jmAI=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@types/esrecurse": "^4.3.1", + "@types/estree": "^1.0.8", + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "11.2.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/espree/-/espree-11.2.0.tgz", + "integrity": "sha1-AdXkfcMyqrowWQCDYkVKjMNMyqU=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.16.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^5.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/exenv-es6": { "version": "1.1.1", "resolved": "http://localhost:4873/exenv-es6/-/exenv-es6-1.1.1.tgz", "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", "license": "MIT" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", + "dev": true, + "license": "MIT" + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha1-9cI8EH8PN96NvfJPE3IrO5jVJyY=", + "dev": true, + "license": "ISC" + }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "http://localhost:4873/fs-extra/-/fs-extra-10.1.0.tgz", @@ -716,6 +1234,19 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "http://localhost:4873/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -723,6 +1254,26 @@ "dev": true, "license": "ISC" }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/inherits": { "version": "2.0.3", "resolved": "http://localhost:4873/inherits/-/inherits-2.0.3.tgz", @@ -730,6 +1281,57 @@ "dev": true, "license": "ISC" }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true, + "license": "ISC" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", + "dev": true, + "license": "MIT" + }, "node_modules/jsonfile": { "version": "6.2.0", "resolved": "http://localhost:4873/jsonfile/-/jsonfile-6.2.0.tgz", @@ -743,6 +1345,46 @@ "graceful-fs": "^4.1.6" } }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/levn/-/levn-0.4.1.tgz", + "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/lru-cache": { "version": "11.3.5", "resolved": "http://localhost:4873/lru-cache/-/lru-cache-11.3.5.tgz", @@ -779,6 +1421,70 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ms/-/ms-2.1.3.tgz", + "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", + "dev": true, + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "http://localhost:4873/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -797,6 +1503,26 @@ "util": "^0.10.3" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "http://localhost:4873/path-scurry/-/path-scurry-2.0.2.tgz", @@ -814,6 +1540,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/process": { "version": "0.11.10", "resolved": "http://localhost:4873/process/-/process-0.11.10.tgz", @@ -824,6 +1560,16 @@ "node": ">= 0.6.0" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/rimraf": { "version": "6.1.3", "resolved": "http://localhost:4873/rimraf/-/rimraf-6.1.3.tgz", @@ -854,11 +1600,28 @@ "convert-string": "~0.1.0" } }, - "node_modules/tabbable": { - "version": "6.4.0", - "resolved": "http://localhost:4873/tabbable/-/tabbable-6.4.0.tgz", - "integrity": "sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg==", - "license": "MIT" + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, "node_modules/tslib": { "version": "2.8.1", @@ -866,6 +1629,19 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, "node_modules/universalify": { "version": "2.0.1", "resolved": "http://localhost:4873/universalify/-/universalify-2.0.1.tgz", @@ -876,6 +1652,16 @@ "node": ">= 10.0.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, "node_modules/util": { "version": "0.10.4", "resolved": "http://localhost:4873/util/-/util-0.10.4.tgz", @@ -885,6 +1671,45 @@ "dependencies": { "inherits": "2.0.3" } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/which/-/which-2.0.2.tgz", + "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } } } } From 4bcbbb29249fecb6f05c2808342b019faaaf0b98 Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Tue, 12 May 2026 11:14:25 +0200 Subject: [PATCH 19/60] Update package-lock.json to use secure package URLs and update versions --- src/Charts.Scripts/package-lock.json | 147 +++++++++++++-------------- 1 file changed, 73 insertions(+), 74 deletions(-) diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json index 53826d5b9c..c00befe0de 100644 --- a/src/Charts.Scripts/package-lock.json +++ b/src/Charts.Scripts/package-lock.json @@ -1,6 +1,5 @@ { "name": "microsoft.fluentui.aspnetcore.components.charts.assets", - "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { @@ -577,7 +576,7 @@ }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", - "resolved": "http://localhost:4873/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", "integrity": "sha1-T4RsHk/Ns8qA6zGALEo2bVWZsw4=", "license": "MIT", "dependencies": { @@ -668,15 +667,15 @@ } }, "node_modules/@microsoft/fast-element": { - "version": "2.0.0", - "resolved": "http://localhost:4873/@microsoft/fast-element/-/fast-element-2.0.0.tgz", - "integrity": "sha512-Tzv4dCGTg10NNyqWWGRy3bYG4vacQUapjy5gpdpmFbSgvNF8aOzJJMkG5CfVUDoC9gWxY1ZyGgjXX0p86ZXX5w==", + "version": "2.10.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/fast-element/-/fast-element-2.10.4.tgz", + "integrity": "sha1-ErjGyQkC8qVNWyf2GNfQleEzVG0=", "license": "MIT" }, "node_modules/@microsoft/fast-web-utilities": { "version": "6.0.0", - "resolved": "http://localhost:4873/@microsoft/fast-web-utilities/-/fast-web-utilities-6.0.0.tgz", - "integrity": "sha512-ckCA4Xn91ja1Qz+jhGGL1Q3ZeuRpA5VvYcRA7GzA1NP545sl14bwz3tbHCq8jIk+PL7mkSaIveGMYuJB2L4Izg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/fast-web-utilities/-/fast-web-utilities-6.0.0.tgz", + "integrity": "sha1-dnjCss0Sru94X04iiNqTpNueOKY=", "license": "MIT", "dependencies": { "exenv-es6": "^1.1.1" @@ -691,8 +690,8 @@ }, "node_modules/@swc/helpers": { "version": "0.5.21", - "resolved": "http://localhost:4873/@swc/helpers/-/helpers-0.5.21.tgz", - "integrity": "sha512-jI/VAmtdjB/RnI8GTnokyX7Ug8c+g+ffD6QRLa6XQewtnGyukKkKSk3wLTM3b5cjt1jNh9x0jfVlagdN2gDKQg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha1-CxsCAxfuEoKGDKZvfpp8d5DwWuA=", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -700,29 +699,29 @@ }, "node_modules/@types/d3-format": { "version": "3.0.4", - "resolved": "http://localhost:4873/@types/d3-format/-/d3-format-3.0.4.tgz", - "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha1-seRGVkTds/3zomP+uyQKbNYW3pA=", "dev": true, "license": "MIT" }, "node_modules/@types/d3-path": { "version": "3.1.1", - "resolved": "http://localhost:4873/@types/d3-path/-/d3-path-3.1.1.tgz", - "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha1-9jKzgMOsoduo40qgSbzWpK8j34o=", "dev": true, "license": "MIT" }, "node_modules/@types/d3-selection": { "version": "3.0.11", - "resolved": "http://localhost:4873/@types/d3-selection/-/d3-selection-3.0.11.tgz", - "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha1-vXpF/AqMMWemMWdeYbwsorBY1KM=", "dev": true, "license": "MIT" }, "node_modules/@types/d3-shape": { "version": "3.1.8", - "resolved": "http://localhost:4873/@types/d3-shape/-/d3-shape-3.1.8.tgz", - "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha1-0VFsxQh1O+BoUs0GdY47tUoisOM=", "dev": true, "license": "MIT", "dependencies": { @@ -792,8 +791,8 @@ }, "node_modules/balanced-match": { "version": "4.0.4", - "resolved": "http://localhost:4873/balanced-match/-/balanced-match-4.0.4.tgz", - "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha1-v7EGYv7tgZaixi58aOF3IMJ0F5o=", "dev": true, "license": "MIT", "engines": { @@ -801,9 +800,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "http://localhost:4873/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "version": "5.0.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha1-7Gj+CmQaKdhxFXnK9kHQW64fIoU=", "dev": true, "license": "MIT", "dependencies": { @@ -815,14 +814,14 @@ }, "node_modules/convert-hex": { "version": "0.1.0", - "resolved": "http://localhost:4873/convert-hex/-/convert-hex-0.1.0.tgz", - "integrity": "sha512-w20BOb1PiR/sEJdS6wNrUjF5CSfscZFUp7R9NSlXH8h2wynzXVEPFPJECAnkNylZ+cvf3p7TyRUHggDmrwXT9A==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/convert-hex/-/convert-hex-0.1.0.tgz", + "integrity": "sha1-CMBFaJIsJ3drii6BqV05M2LqC2U=", "dev": true }, "node_modules/convert-string": { "version": "0.1.0", - "resolved": "http://localhost:4873/convert-string/-/convert-string-0.1.0.tgz", - "integrity": "sha512-1KX9ESmtl8xpT2LN2tFnKSbV4NiarbVi8DVb39ZriijvtTklyrT+4dT1wsGMHKD3CJUjXgvJzstm9qL9ICojGA==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/convert-string/-/convert-string-0.1.0.tgz", + "integrity": "sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo=", "dev": true }, "node_modules/cross-spawn": { @@ -842,8 +841,8 @@ }, "node_modules/d3-format": { "version": "3.1.2", - "resolved": "http://localhost:4873/d3-format/-/d3-format-3.1.2.tgz", - "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha1-Af20a1i+sfVbELQq1wtuNE1esq4=", "license": "ISC", "engines": { "node": ">=12" @@ -851,8 +850,8 @@ }, "node_modules/d3-path": { "version": "3.1.0", - "resolved": "http://localhost:4873/d3-path/-/d3-path-3.1.0.tgz", - "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha1-It+TkDL7WnGuixgA1h3beFHEJSY=", "license": "ISC", "engines": { "node": ">=12" @@ -860,8 +859,8 @@ }, "node_modules/d3-selection": { "version": "3.0.0", - "resolved": "http://localhost:4873/d3-selection/-/d3-selection-3.0.0.tgz", - "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha1-wlM4IH76csxbm9FFihpBkB8eGzE=", "license": "ISC", "engines": { "node": ">=12" @@ -869,8 +868,8 @@ }, "node_modules/d3-shape": { "version": "3.2.0", - "resolved": "http://localhost:4873/d3-shape/-/d3-shape-3.2.0.tgz", - "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha1-oag5y9m6RfKGdMadf4Vbz5HfxqU=", "license": "ISC", "dependencies": { "d3-path": "^3.1.0" @@ -948,8 +947,8 @@ }, "node_modules/esbuild-plugin-inline-css": { "version": "0.0.1", - "resolved": "http://localhost:4873/esbuild-plugin-inline-css/-/esbuild-plugin-inline-css-0.0.1.tgz", - "integrity": "sha512-CF2NPh3RbSdS6n3KgDmi67MhiBjjYJ+x+xi6Ha3ikMrO4owoS0Oj3+QQpIgwAOfTinLyNLbwhMOlht4QE/iqlQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esbuild-plugin-inline-css/-/esbuild-plugin-inline-css-0.0.1.tgz", + "integrity": "sha1-LhPMtN+xsItXDgin3zKK4x9SZQ8=", "dev": true, "license": "MIT", "dependencies": { @@ -1125,8 +1124,8 @@ }, "node_modules/exenv-es6": { "version": "1.1.1", - "resolved": "http://localhost:4873/exenv-es6/-/exenv-es6-1.1.1.tgz", - "integrity": "sha512-vlVu3N8d6yEMpMsEm+7sUBAI81aqYYuEvfK0jNqmdb/OPXzzH7QWDDnVjMvDSY47JdHEqx/dfC/q8WkfoTmpGQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/exenv-es6/-/exenv-es6-1.1.1.tgz", + "integrity": "sha1-gLeoxa8k1TMx91W6wH6Eq7H23mc=", "license": "MIT" }, "node_modules/fast-deep-equal": { @@ -1203,8 +1202,8 @@ }, "node_modules/fs-extra": { "version": "10.1.0", - "resolved": "http://localhost:4873/fs-extra/-/fs-extra-10.1.0.tgz", - "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fs-extra/-/fs-extra-10.1.0.tgz", + "integrity": "sha1-Aoc8+8QITd4SfqpfmQXu8jJdGr8=", "dev": true, "license": "MIT", "dependencies": { @@ -1218,8 +1217,8 @@ }, "node_modules/glob": { "version": "13.0.6", - "resolved": "http://localhost:4873/glob/-/glob-13.0.6.tgz", - "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/glob/-/glob-13.0.6.tgz", + "integrity": "sha1-B4ZmVmpCUUfMrPvS4zLetmor5x0=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1249,8 +1248,8 @@ }, "node_modules/graceful-fs": { "version": "4.2.11", - "resolved": "http://localhost:4873/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha1-QYPk6L8Iu24Fu7L30uDI9xLKQOM=", "dev": true, "license": "ISC" }, @@ -1276,8 +1275,8 @@ }, "node_modules/inherits": { "version": "2.0.3", - "resolved": "http://localhost:4873/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", "dev": true, "license": "ISC" }, @@ -1333,9 +1332,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "http://localhost:4873/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "version": "6.2.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha1-tuMXF/Isw3MwsIHOAFHtXeU68vY=", "dev": true, "license": "MIT", "dependencies": { @@ -1386,9 +1385,9 @@ } }, "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "http://localhost:4873/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "version": "11.3.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha1-8DBq1unwpdwlsWrrpOj1e37C31U=", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -1397,8 +1396,8 @@ }, "node_modules/minimatch": { "version": "10.2.5", - "resolved": "http://localhost:4873/minimatch/-/minimatch-10.2.5.tgz", - "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha1-vUhoegvjjtKWE5kQVgD4MglYYdE=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1413,8 +1412,8 @@ }, "node_modules/minipass": { "version": "7.1.3", - "resolved": "http://localhost:4873/minipass/-/minipass-7.1.3.tgz", - "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha1-eTibTrG7LQA6m7qH1JLyvTe9xls=", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -1487,15 +1486,15 @@ }, "node_modules/package-json-from-dist": { "version": "1.0.1", - "resolved": "http://localhost:4873/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", - "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha1-TxRxoBCCeob5TP2bByfjbSZ95QU=", "dev": true, "license": "BlueOak-1.0.0" }, "node_modules/path": { "version": "0.12.7", - "resolved": "http://localhost:4873/path/-/path-0.12.7.tgz", - "integrity": "sha512-aXXC6s+1w7otVF9UletFkFcDsJeO7lSZBPUQhtb5O0xJe8LtYhj/GxldoL09bBj9+ZmE2hNoHqQSFMN5fikh4Q==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path/-/path-0.12.7.tgz", + "integrity": "sha1-1NwqUGxM4hl+tIHr/NWzbAFAsQ8=", "dev": true, "license": "MIT", "dependencies": { @@ -1525,8 +1524,8 @@ }, "node_modules/path-scurry": { "version": "2.0.2", - "resolved": "http://localhost:4873/path-scurry/-/path-scurry-2.0.2.tgz", - "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha1-a+DQ7gKhDZ4N56mLrmXhgskGH4U=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1552,8 +1551,8 @@ }, "node_modules/process": { "version": "0.11.10", - "resolved": "http://localhost:4873/process/-/process-0.11.10.tgz", - "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/process/-/process-0.11.10.tgz", + "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", "dev": true, "license": "MIT", "engines": { @@ -1572,8 +1571,8 @@ }, "node_modules/rimraf": { "version": "6.1.3", - "resolved": "http://localhost:4873/rimraf/-/rimraf-6.1.3.tgz", - "integrity": "sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/rimraf/-/rimraf-6.1.3.tgz", + "integrity": "sha1-r77iNrO9K+Mx1OfORJO6wXGJga8=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { @@ -1592,8 +1591,8 @@ }, "node_modules/sha256": { "version": "0.2.0", - "resolved": "http://localhost:4873/sha256/-/sha256-0.2.0.tgz", - "integrity": "sha512-kTWMJUaez5iiT9CcMv8jSq6kMhw3ST0uRdcIWl3D77s6AsLXNXRp3heeqqfu5+Dyfu4hwpQnMzhqHh8iNQxw0w==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/sha256/-/sha256-0.2.0.tgz", + "integrity": "sha1-c6C0GNqrcDW/+G6EkeNjQS/CqwU=", "dev": true, "dependencies": { "convert-hex": "~0.1.0", @@ -1625,8 +1624,8 @@ }, "node_modules/tslib": { "version": "2.8.1", - "resolved": "http://localhost:4873/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "license": "0BSD" }, "node_modules/type-check": { @@ -1644,8 +1643,8 @@ }, "node_modules/universalify": { "version": "2.0.1", - "resolved": "http://localhost:4873/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha1-Fo78IYCWTmOG0GHglN9hr+I5sY0=", "dev": true, "license": "MIT", "engines": { @@ -1664,8 +1663,8 @@ }, "node_modules/util": { "version": "0.10.4", - "resolved": "http://localhost:4873/util/-/util-0.10.4.tgz", - "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/util/-/util-0.10.4.tgz", + "integrity": "sha1-OqASW/5mikZy3liFfTrOJ+y3aQE=", "dev": true, "license": "MIT", "dependencies": { From 90403ef4124c18b1bf7a14e8ffb4f17f87172ebd Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Tue, 12 May 2026 11:31:26 +0200 Subject: [PATCH 20/60] Update .npmrc to use secure Azure DevOps package registry --- src/Core.Scripts/.npmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core.Scripts/.npmrc b/src/Core.Scripts/.npmrc index 642288e0b9..a8f33531f6 100644 --- a/src/Core.Scripts/.npmrc +++ b/src/Core.Scripts/.npmrc @@ -1,3 +1,3 @@ -registry=http://localhost:4873/ +registry=https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ always-auth=true From bb84c06954d452dbcd93feb95dfdcf8a908db0ea Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Tue, 12 May 2026 11:32:45 +0200 Subject: [PATCH 21/60] Remove unnecessary esbuild platform-specific packages from package-lock.json --- src/Core.Scripts/package-lock.json | 425 ----------------------------- 1 file changed, 425 deletions(-) diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json index 479fdda21d..831223b729 100644 --- a/src/Core.Scripts/package-lock.json +++ b/src/Core.Scripts/package-lock.json @@ -36,431 +36,6 @@ "node": ">=6.9.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha1-eiicFY4py/WeoK/IPMgPBtHIlAI=", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha1-XsGEdgXgW12+XfkNuf9+PkxY3Kc=", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha1-uIKNnt+jqSZgZE643m5PPCA9exc=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha1-OQZCF1uI74K61MzgP4qxP+mxkS4=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha1-rkUyWWDVlQzWlR5PlzlvTh/32NM=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha1-wHkkfViba5lEllnZTwaVG4S/8uQ=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha1-RcRWIVpIZZPJSQApcgLcEciAo3o=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha1-A5lJTByF5DiOm3BAvWDUjypbDSw=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha1-e0L/qEwoiulP3EMcGyionjw7kng=", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha1-1tnwnvDeVBFr9Fmk1TysfglS/jk=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha1-3rFdES7Y3WBTRra5U9I6If+BJT8=", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha1-gfuJ0H7sx5sVfephAzdXcm/ODKQ=", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha1-0OQmkbP/evn7Ihe3D8AfNDvbYrs=", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha1-OJ8+XpjxfUd8RnzIcTbhoHburYc=", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha1-djvWDVmyQr4S2h5n1XKfMCTGBfo=", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha1-qsYGFjSHLkZ33mk7zoAw1zsf0FU=", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha1-TykXdHGI/ndjK87GWy2EtCJBl3k=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha1-gU3wrlegw4aBRJG4OX7rqCCUqUc=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha1-4BvffmD6GgjkbUbZYLDZu4rCEK8=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha1-ShXDaqzKaNLVpMkLcQwGdZ9MH/o=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha1-R15hAUmKjszjAI18OIER16J8F70=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha1-z9w5V/C3pp8b3hKarRf8wvb6Az4=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha1-oBPIVv7KzRw67Jhciv4dHLAXSX0=", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha1-6uBeDzUnHK04mLQxaNPpo7uvR+U=", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha1-BhYevFv3XAjWn+s8ayJWBRWROZg=", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/win32-x64": { "version": "0.28.0", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", From 68302e8b9bd476d187024c3f64a6a11e06f029c4 Mon Sep 17 00:00:00 2001 From: Denis VOITURON Date: Tue, 12 May 2026 11:41:27 +0200 Subject: [PATCH 22/60] Update package-lock.json to upgrade dependencies and add new platform-specific packages --- src/Core.Scripts/package-lock.json | 485 +++++++++++++++++++++++++++-- 1 file changed, 455 insertions(+), 30 deletions(-) diff --git a/src/Core.Scripts/package-lock.json b/src/Core.Scripts/package-lock.json index 831223b729..19eec5d437 100644 --- a/src/Core.Scripts/package-lock.json +++ b/src/Core.Scripts/package-lock.json @@ -24,18 +24,443 @@ } }, "node_modules/@babel/runtime-corejs3": { - "version": "7.28.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", - "integrity": "sha1-wlvjnHmXzi8TDXC5uuy47ZTfk/o=", + "version": "7.29.2", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@babel/runtime-corejs3/-/runtime-corejs3-7.29.2.tgz", + "integrity": "sha1-y4atBueh05Ikr7Eqh0MBCF4HGEY=", "dev": true, "license": "MIT", "dependencies": { - "core-js-pure": "^3.43.0" + "core-js-pure": "^3.48.0" }, "engines": { "node": ">=6.9.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", + "integrity": "sha1-eiicFY4py/WeoK/IPMgPBtHIlAI=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm/-/android-arm-0.28.0.tgz", + "integrity": "sha1-XsGEdgXgW12+XfkNuf9+PkxY3Kc=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", + "integrity": "sha1-uIKNnt+jqSZgZE643m5PPCA9exc=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/android-x64/-/android-x64-0.28.0.tgz", + "integrity": "sha1-OQZCF1uI74K61MzgP4qxP+mxkS4=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", + "integrity": "sha1-rkUyWWDVlQzWlR5PlzlvTh/32NM=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", + "integrity": "sha1-wHkkfViba5lEllnZTwaVG4S/8uQ=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", + "integrity": "sha1-RcRWIVpIZZPJSQApcgLcEciAo3o=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", + "integrity": "sha1-A5lJTByF5DiOm3BAvWDUjypbDSw=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", + "integrity": "sha1-e0L/qEwoiulP3EMcGyionjw7kng=", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", + "integrity": "sha1-1tnwnvDeVBFr9Fmk1TysfglS/jk=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", + "integrity": "sha1-3rFdES7Y3WBTRra5U9I6If+BJT8=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", + "integrity": "sha1-gfuJ0H7sx5sVfephAzdXcm/ODKQ=", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", + "integrity": "sha1-0OQmkbP/evn7Ihe3D8AfNDvbYrs=", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", + "integrity": "sha1-OJ8+XpjxfUd8RnzIcTbhoHburYc=", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", + "integrity": "sha1-djvWDVmyQr4S2h5n1XKfMCTGBfo=", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", + "integrity": "sha1-qsYGFjSHLkZ33mk7zoAw1zsf0FU=", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", + "integrity": "sha1-TykXdHGI/ndjK87GWy2EtCJBl3k=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", + "integrity": "sha1-gU3wrlegw4aBRJG4OX7rqCCUqUc=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", + "integrity": "sha1-4BvffmD6GgjkbUbZYLDZu4rCEK8=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", + "integrity": "sha1-ShXDaqzKaNLVpMkLcQwGdZ9MH/o=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", + "integrity": "sha1-R15hAUmKjszjAI18OIER16J8F70=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", + "integrity": "sha1-z9w5V/C3pp8b3hKarRf8wvb6Az4=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", + "integrity": "sha1-oBPIVv7KzRw67Jhciv4dHLAXSX0=", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", + "integrity": "sha1-6uBeDzUnHK04mLQxaNPpo7uvR+U=", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", + "integrity": "sha1-BhYevFv3XAjWn+s8ayJWBRWROZg=", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@esbuild/win32-x64": { "version": "0.28.0", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", @@ -240,9 +665,9 @@ } }, "node_modules/@microsoft/fast-element": { - "version": "2.9.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/fast-element/-/fast-element-2.9.1.tgz", - "integrity": "sha1-nDS+WK6BfifUgmnv/cPOctSneHA=", + "version": "2.10.4", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/fast-element/-/fast-element-2.10.4.tgz", + "integrity": "sha1-ErjGyQkC8qVNWyf2GNfQleEzVG0=", "license": "MIT", "peer": true }, @@ -254,9 +679,9 @@ "peer": true }, "node_modules/@swc/helpers": { - "version": "0.5.18", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@swc/helpers/-/helpers-0.5.18.tgz", - "integrity": "sha1-/u6r6g0QEG7iWq+QAWXfkRq207E=", + "version": "0.5.21", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@swc/helpers/-/helpers-0.5.21.tgz", + "integrity": "sha1-CxsCAxfuEoKGDKZvfpp8d5DwWuA=", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -278,15 +703,15 @@ }, "node_modules/@types/json-schema": { "version": "7.0.15", - "resolved": "http://localhost:4873/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", "dev": true, "license": "MIT" }, "node_modules/@types/sortablejs": { "version": "1.15.9", - "resolved": "https://registry.npmjs.org/@types/sortablejs/-/sortablejs-1.15.9.tgz", - "integrity": "sha512-7HP+rZGE2p886PKV9c9OJzLBI6BBJu1O7lJGYnPyG3fS4/duUCcngkNCjsLwIMV+WMqANe3tt4irrXHSIe68OQ==", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/sortablejs/-/sortablejs-1.15.9.tgz", + "integrity": "sha1-gtIzf1TU24J5FNgN7lz2VTmuPIs=", "dev": true, "license": "MIT" }, @@ -574,9 +999,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/brace-expansion/-/brace-expansion-5.0.5.tgz", - "integrity": "sha1-3MOjcRa3nz4bRtuZTO1dVw6TD9s=", + "version": "5.0.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha1-7Gj+CmQaKdhxFXnK9kHQW64fIoU=", "dev": true, "license": "MIT", "dependencies": { @@ -599,9 +1024,9 @@ "dev": true }, "node_modules/core-js-pure": { - "version": "3.47.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/core-js-pure/-/core-js-pure-3.47.0.tgz", - "integrity": "sha1-EQTfijtuuRifzFWbWmW5D2bn6Ic=", + "version": "3.49.0", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/core-js-pure/-/core-js-pure-3.49.0.tgz", + "integrity": "sha1-/4Q2tyUaODL1/bvj4Q9/LljlH7E=", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1140,9 +1565,9 @@ "license": "MIT" }, "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha1-fCZb0bZd5pd0eDAAh8mfHIQ4P2I=", + "version": "6.2.1", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/jsonfile/-/jsonfile-6.2.1.tgz", + "integrity": "sha1-tuMXF/Isw3MwsIHOAFHtXeU68vY=", "dev": true, "license": "MIT", "dependencies": { @@ -1193,9 +1618,9 @@ } }, "node_modules/lru-cache": { - "version": "11.2.6", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/lru-cache/-/lru-cache-11.2.6.tgz", - "integrity": "sha1-NWv4op6Ip6KUVQezH2QpploZLFg=", + "version": "11.3.6", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/lru-cache/-/lru-cache-11.3.6.tgz", + "integrity": "sha1-8DBq1unwpdwlsWrrpOj1e37C31U=", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -1203,13 +1628,13 @@ } }, "node_modules/minimatch": { - "version": "10.2.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.4.tgz", - "integrity": "sha1-Rls6zL0CGLgoH1MB4nztxpf5b94=", + "version": "10.2.5", + "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha1-vUhoegvjjtKWE5kQVgD4MglYYdE=", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^5.0.2" + "brace-expansion": "^5.0.5" }, "engines": { "node": "18 || 20 || >=22" From e7280c71a595988296d2bea80797375edc9d390c Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 12:26:03 +0200 Subject: [PATCH 23/60] Add Charts csproj to build-core-lib --- .github/workflows/build-core-lib.yml | 1 + src/Charts.Scripts/package-lock.json | 825 --------------------------- src/Charts.Scripts/package.json | 1 - 3 files changed, 1 insertion(+), 826 deletions(-) diff --git a/.github/workflows/build-core-lib.yml b/.github/workflows/build-core-lib.yml index 30ad1cc41c..b4036172ce 100644 --- a/.github/workflows/build-core-lib.yml +++ b/.github/workflows/build-core-lib.yml @@ -25,6 +25,7 @@ on: env: PROJECTS: > ./src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj + ./src/Core/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj ./src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj TESTS: > ./tests/Core/Components.Tests.csproj diff --git a/src/Charts.Scripts/package-lock.json b/src/Charts.Scripts/package-lock.json index c00befe0de..d68f01560a 100644 --- a/src/Charts.Scripts/package-lock.json +++ b/src/Charts.Scripts/package-lock.json @@ -20,7 +20,6 @@ "@types/d3-shape": "^3.0.0", "esbuild": "0.28.0", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.3.0", "glob": "^13.0.6", "rimraf": "6.1.3" } @@ -467,113 +466,6 @@ "node": ">=18" } }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", - "integrity": "sha1-TpCvZ7xR3e5s3vUoTt9XLsN2tZU=", - "dev": true, - "license": "MIT", - "dependencies": { - "eslint-visitor-keys": "^3.4.3" - }, - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - }, - "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" - } - }, - "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { - "version": "3.4.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", - "integrity": "sha1-DNcv6FUOPC6uFWqWpN3c0cisWAA=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", - "integrity": "sha1-vM32Fbz3tujbgw7AuNIcmiXeWXs=", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" - } - }, - "node_modules/@eslint/config-array": { - "version": "0.23.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-array/-/config-array-0.23.5.tgz", - "integrity": "sha1-VuhtJDBJGV2KzAwGobPf3D+j3pU=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/object-schema": "^3.0.5", - "debug": "^4.3.1", - "minimatch": "^10.2.4" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha1-rhYTTkeSrF+9xTNUiiSsHqn3864=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/core": { - "version": "1.2.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/core/-/core-1.2.1.tgz", - "integrity": "sha1-wdp80bgvqHh/mLVin7gRhIobY84=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@types/json-schema": "^7.0.15" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/object-schema": { - "version": "3.0.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/object-schema/-/object-schema-3.0.5.tgz", - "integrity": "sha1-iOm/TRHSsZwILnjr586IckpesJE=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, - "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha1-xBJf0BXs7rCbeTEJ/bzU3QoC00Y=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@eslint/core": "^1.2.1", - "levn": "^0.4.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - } - }, "node_modules/@fluentui/tokens": { "version": "1.0.0-alpha.23", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@fluentui/tokens/-/tokens-1.0.0-alpha.23.tgz", @@ -600,72 +492,6 @@ "@microsoft/focusgroup-polyfill": "^1.4.1" } }, - "node_modules/@humanfs/core": { - "version": "0.19.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/core/-/core-0.19.2.tgz", - "integrity": "sha1-qCcsoDsqz0kmcCIrIyC2xCG/3mA=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/types": "^0.15.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/node": { - "version": "0.16.8", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/node/-/node-0.16.8.tgz", - "integrity": "sha1-j4AMzME/T4zTEW4tnAqUk52j4+0=", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@humanfs/core": "^0.19.2", - "@humanfs/types": "^0.15.0", - "@humanwhocodes/retry": "^0.4.0" - }, - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanfs/types": { - "version": "0.15.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanfs/types/-/types-0.15.0.tgz", - "integrity": "sha1-8qCfYgEjkLK/8/xvskjd7IwJoJA=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18.0" - } - }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha1-r1smkaIrRL6EewyoFkHF+2rQFyw=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, - "node_modules/@humanwhocodes/retry": { - "version": "0.4.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@humanwhocodes/retry/-/retry-0.4.3.tgz", - "integrity": "sha1-wrnS43TuYsWG062+qHGZsdenpro=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" - } - }, "node_modules/@microsoft/fast-element": { "version": "2.10.4", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@microsoft/fast-element/-/fast-element-2.10.4.tgz", @@ -728,67 +554,6 @@ "@types/d3-path": "*" } }, - "node_modules/@types/esrecurse": { - "version": "4.3.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/esrecurse/-/esrecurse-4.3.1.tgz", - "integrity": "sha1-b2Nq+WL75hkbgwvWdrpZhpJrzOw=", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/estree": { - "version": "1.0.9", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/estree/-/estree-1.0.9.tgz", - "integrity": "sha1-zz8Oh2177hWpOrkluCv1cKOQSiQ=", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha1-WWoXRyM2lNUPatinhp/Lb1bPWEE=", - "dev": true, - "license": "MIT" - }, - "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha1-TOecib5Ar+ev6POtuQKh8c6awIo=", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha1-ftW7VZCLOy8bxVxq8WU7rafweTc=", - "dev": true, - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/ajv": { - "version": "6.15.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ajv/-/ajv-6.15.0.tgz", - "integrity": "sha1-B+mCx0YmFnqnoklcU4F4ktcTlJI=", - "dev": true, - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, "node_modules/balanced-match": { "version": "4.0.4", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/balanced-match/-/balanced-match-4.0.4.tgz", @@ -824,21 +589,6 @@ "integrity": "sha1-ec5BqbsNA7z3LNxqjzxW+7xkQQo=", "dev": true }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha1-ilj+ePANzXDDcEUXWd+/rwPo7p8=", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, "node_modules/d3-format": { "version": "3.1.2", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/d3-format/-/d3-format-3.1.2.tgz", @@ -878,31 +628,6 @@ "node": ">=12" } }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/debug/-/debug-4.4.3.tgz", - "integrity": "sha1-xq5DLZvZZiWC/OCHCbA4xY6ePWo=", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/deep-is": { - "version": "0.1.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/deep-is/-/deep-is-0.1.4.tgz", - "integrity": "sha1-pvLc5hL63S7x9Rm3NVHxfoUZmDE=", - "dev": true, - "license": "MIT" - }, "node_modules/esbuild": { "version": "0.28.0", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esbuild/-/esbuild-0.28.0.tgz", @@ -957,249 +682,12 @@ "sha256": "^0.2.0" } }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha1-FLqDpdNz49MR5a/KKc9b+tllvzQ=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint": { - "version": "10.3.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint/-/eslint-10.3.0.tgz", - "integrity": "sha1-7VuBDrjgGRvyS93PnNtFuXTgoW0=", - "dev": true, - "license": "MIT", - "dependencies": { - "@eslint-community/eslint-utils": "^4.8.0", - "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.5", - "@eslint/config-helpers": "^0.5.5", - "@eslint/core": "^1.2.1", - "@eslint/plugin-kit": "^0.7.1", - "@humanfs/node": "^0.16.6", - "@humanwhocodes/module-importer": "^1.0.1", - "@humanwhocodes/retry": "^0.4.2", - "@types/estree": "^1.0.6", - "ajv": "^6.14.0", - "cross-spawn": "^7.0.6", - "debug": "^4.3.2", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^9.1.2", - "eslint-visitor-keys": "^5.0.1", - "espree": "^11.2.0", - "esquery": "^1.7.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", - "find-up": "^5.0.0", - "glob-parent": "^6.0.2", - "ignore": "^5.2.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.3" - }, - "bin": { - "eslint": "bin/eslint.js" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://eslint.org/donate" - }, - "peerDependencies": { - "jiti": "*" - }, - "peerDependenciesMeta": { - "jiti": { - "optional": true - } - } - }, - "node_modules/eslint-scope": { - "version": "9.1.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-scope/-/eslint-scope-9.1.2.tgz", - "integrity": "sha1-ud5qzi+rHP8k0uWNhbdMj86jmAI=", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "@types/esrecurse": "^4.3.1", - "@types/estree": "^1.0.8", - "esrecurse": "^4.3.0", - "estraverse": "^5.2.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/eslint-visitor-keys": { - "version": "5.0.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", - "integrity": "sha1-njyUiWl4JNLUzjqK0SYo+R6fWb4=", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/espree": { - "version": "11.2.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/espree/-/espree-11.2.0.tgz", - "integrity": "sha1-AdXkfcMyqrowWQCDYkVKjMNMyqU=", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "acorn": "^8.16.0", - "acorn-jsx": "^5.3.2", - "eslint-visitor-keys": "^5.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24" - }, - "funding": { - "url": "https://opencollective.com/eslint" - } - }, - "node_modules/esquery": { - "version": "1.7.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esquery/-/esquery-1.7.0.tgz", - "integrity": "sha1-CNBI8mHw3e21uulfRoCUY9nJSW0=", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "estraverse": "^5.1.0" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha1-eteWTWeauyi+5yzsY3WLHF0smSE=", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha1-LupSkHAvJquP5TcDcP+GyWXSESM=", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha1-dNLrTeC42hKTcRkQ1Qd1ubcQ72Q=", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/exenv-es6": { "version": "1.1.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/exenv-es6/-/exenv-es6-1.1.1.tgz", "integrity": "sha1-gLeoxa8k1TMx91W6wH6Eq7H23mc=", "license": "MIT" }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha1-On1WtVnWy8PrUSMlJE5hmmXGxSU=", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha1-h0v2nG9ATCtdmcSBNBOZ/VWJJjM=", - "dev": true, - "license": "MIT" - }, - "node_modules/fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true, - "license": "MIT" - }, - "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha1-d4e93PETG/+5JjbGlFe7wO3W2B8=", - "dev": true, - "license": "MIT", - "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/find-up": { - "version": "5.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/find-up/-/find-up-5.0.0.tgz", - "integrity": "sha1-TJKBnstwg1YeT0okCoa+UZj1Nvw=", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^6.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha1-Ds45/LFO4BL0sEEL0z3ZwfAREnw=", - "dev": true, - "license": "MIT", - "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha1-9cI8EH8PN96NvfJPE3IrO5jVJyY=", - "dev": true, - "license": "ISC" - }, "node_modules/fs-extra": { "version": "10.1.0", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/fs-extra/-/fs-extra-10.1.0.tgz", @@ -1233,19 +721,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha1-bSN9mQg5UMeSkPJMdkKj3poo+eM=", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -1253,26 +728,6 @@ "dev": true, "license": "ISC" }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha1-PNQOcp82Q/2HywTlC/DrcivFlvU=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, "node_modules/inherits": { "version": "2.0.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/inherits/-/inherits-2.0.3.tgz", @@ -1280,57 +735,6 @@ "dev": true, "license": "ISC" }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha1-ZPYeQsu7LuwgcanawLKLoeZdUIQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", - "dev": true, - "license": "ISC" - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha1-kziAKjDTtmBfvgYT4JQAjKjAWhM=", - "dev": true, - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", - "dev": true, - "license": "MIT" - }, - "node_modules/json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true, - "license": "MIT" - }, "node_modules/jsonfile": { "version": "6.2.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/jsonfile/-/jsonfile-6.2.1.tgz", @@ -1344,46 +748,6 @@ "graceful-fs": "^4.1.6" } }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha1-qHmpnilFL5QkOfKkBeOvizHU3pM=", - "dev": true, - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/levn": { - "version": "0.4.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/levn/-/levn-0.4.1.tgz", - "integrity": "sha1-rkViwAdHO5MqYgDUAyaN0v/8at4=", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/locate-path": { - "version": "6.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/locate-path/-/locate-path-6.0.0.tgz", - "integrity": "sha1-VTIeswn+u8WcSAHZMackUqaB0oY=", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^5.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/lru-cache": { "version": "11.3.6", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/lru-cache/-/lru-cache-11.3.6.tgz", @@ -1420,70 +784,6 @@ "node": ">=16 || 14 >=14.17" } }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ms/-/ms-2.1.3.tgz", - "integrity": "sha1-V0yBOM4dK1hh8LRFedut1gxmFbI=", - "dev": true, - "license": "MIT" - }, - "node_modules/natural-compare": { - "version": "1.4.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true, - "license": "MIT" - }, - "node_modules/optionator": { - "version": "0.9.4", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/optionator/-/optionator-0.9.4.tgz", - "integrity": "sha1-fqHBpdkddk+yghOciP4R4YKjpzQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.5" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/p-limit": { - "version": "3.1.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha1-4drMvnjQ0TiMoYxk/qOOPlfjcGs=", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "5.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/p-locate/-/p-locate-5.0.0.tgz", - "integrity": "sha1-g8gxXGeFAF470CGDlBHJ4RDm2DQ=", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/package-json-from-dist": { "version": "1.0.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", @@ -1502,26 +802,6 @@ "util": "^0.10.3" } }, - "node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha1-UTvb4tO5XXdi6METfvoZXGxhtbM=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha1-WB9q3mWMu6ZaDTOA3ndTKVBU83U=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/path-scurry": { "version": "2.0.2", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/path-scurry/-/path-scurry-2.0.2.tgz", @@ -1539,16 +819,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/prelude-ls": { - "version": "1.2.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha1-3rxkidem5rDnYRiIzsiAM30xY5Y=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/process": { "version": "0.11.10", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/process/-/process-0.11.10.tgz", @@ -1559,16 +829,6 @@ "node": ">= 0.6.0" } }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha1-AnQi4vrsCyXhVJw+G9gwm5EztuU=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, "node_modules/rimraf": { "version": "6.1.3", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/rimraf/-/rimraf-6.1.3.tgz", @@ -1599,48 +859,12 @@ "convert-string": "~0.1.0" } }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha1-zNCvT4g1+9wmW4JGGq8MNmY/NOo=", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha1-rhbxZE2HPsrYQ7AwexQzYtTEIXI=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/tslib/-/tslib-2.8.1.tgz", "integrity": "sha1-YS7+TtI11Wfoq6Xypfq3AoCt6D8=", "license": "0BSD" }, - "node_modules/type-check": { - "version": "0.4.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/type-check/-/type-check-0.4.0.tgz", - "integrity": "sha1-B7ggO/pwVsBlcFDjzNLDdzC6uPE=", - "dev": true, - "license": "MIT", - "dependencies": { - "prelude-ls": "^1.2.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, "node_modules/universalify": { "version": "2.0.1", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/universalify/-/universalify-2.0.1.tgz", @@ -1651,16 +875,6 @@ "node": ">= 10.0.0" } }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha1-mxpSWVIlhZ5V9mnZKPiMbFfyp34=", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, "node_modules/util": { "version": "0.10.4", "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/util/-/util-0.10.4.tgz", @@ -1670,45 +884,6 @@ "dependencies": { "inherits": "2.0.3" } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/which/-/which-2.0.2.tgz", - "integrity": "sha1-fGqN0KY2oDJ+ELWckobu6T8/UbE=", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/word-wrap": { - "version": "1.2.5", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/word-wrap/-/word-wrap-1.2.5.tgz", - "integrity": "sha1-0sRcbdT7zmIaZvE2y+Mor9BBCzQ=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "resolved": "https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/yocto-queue/-/yocto-queue-0.1.0.tgz", - "integrity": "sha1-ApTrPe4FAo0x7hpfosVWpqrxChs=", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } } } } diff --git a/src/Charts.Scripts/package.json b/src/Charts.Scripts/package.json index 4c5f80aac6..8b481e9dac 100644 --- a/src/Charts.Scripts/package.json +++ b/src/Charts.Scripts/package.json @@ -26,7 +26,6 @@ "@types/d3-shape": "^3.0.0", "esbuild": "0.28.0", "esbuild-plugin-inline-css": "0.0.1", - "eslint": "10.3.0", "glob": "^13.0.6", "rimraf": "6.1.3" }, From 957e2d423d7071521850ef4e1003f0c076b472f0 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 12:36:21 +0200 Subject: [PATCH 24/60] - Update Microsoft.VisualStudio.JavaScript.Sdk - Re-add Core SDK 9.0.300 build fix --- ...UI.AspNetCore.Components.Charts.Scripts.esproj | 2 +- ....FluentUI.AspNetCore.Components.Scripts.esproj | 2 +- ...icrosoft.FluentUI.AspNetCore.Components.csproj | 15 +++++++-------- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj index 4a6abcb4c0..f31a6e8a9b 100644 --- a/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj +++ b/src/Charts.Scripts/Microsoft.FluentUI.AspNetCore.Components.Charts.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components.Charts diff --git a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj index e8b86aab1a..04c79ebb9f 100644 --- a/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj +++ b/src/Core.Scripts/Microsoft.FluentUI.AspNetCore.Components.Scripts.esproj @@ -1,4 +1,4 @@ - + dist\ Microsoft.FluentUI.AspNetCore.Components diff --git a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj index 08dbd6416d..985fa768b8 100644 --- a/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj +++ b/src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj @@ -104,7 +104,7 @@ - <_ContentWithAbsolutePaths Include="@(Content)" Condition="$([System.String]::Copy('%(Identity)').StartsWith('/'))"> %(Identity) @@ -120,23 +120,22 @@ - --> - --> - --> - --> - - --> + From 71e385191a5f63197919b1cd11efc82108e7c3a4 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 12:42:32 +0200 Subject: [PATCH 25/60] Fix path --- .github/workflows/build-core-lib.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build-core-lib.yml b/.github/workflows/build-core-lib.yml index b4036172ce..ac69bcf8c9 100644 --- a/.github/workflows/build-core-lib.yml +++ b/.github/workflows/build-core-lib.yml @@ -25,7 +25,7 @@ on: env: PROJECTS: > ./src/Core/Microsoft.FluentUI.AspNetCore.Components.csproj - ./src/Core/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj + ./src/Charts/Microsoft.FluentUI.AspNetCore.Components.Charts.csproj ./src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj TESTS: > ./tests/Core/Components.Tests.csproj From afdc3c201439d1eeaee106a35ae7e3cdc4273cca Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 12:57:07 +0200 Subject: [PATCH 26/60] Add charts to DocApiGen --- .../FluentUI.Demo.DocApiGen/Properties/launchSettings.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json index 6db848d549..dedefc38a0 100644 --- a/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json +++ b/examples/Tools/FluentUI.Demo.DocApiGen/Properties/launchSettings.json @@ -5,6 +5,10 @@ "commandName": "Project", "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.xml\" --dll \"$(SolutionDir)src/Core/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments.json\" --format json" }, + "FluentUI.Demo.DocApiGen charts": { + "commandName": "Project", + "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.Charts.xml\" --dll \"$(SolutionDir)src/Charts/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.Charts.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/chart-comments.json\" --format json" + }, "FluentUI.Demo.DocApiGen all": { "commandName": "Project", "commandLineArgs": "--xml \"$(MSBuildProjectDirectory)/Microsoft.FluentUI.AspNetCore.Components.xml\" --dll \"$(SolutionDir)src/Core/bin/$(Configuration)/net9.0/Microsoft.FluentUI.AspNetCore.Components.dll\" --output \"$(SolutionDir)examples/Demo/FluentUI.Demo.Client/wwwroot/api-comments-all.json\" --format json --mode all" From 397f0481c8cbeca08b88da92e1987082100475e7 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 12 May 2026 15:21:42 +0200 Subject: [PATCH 27/60] Update mcp server proj to include charts docs --- ...osoft.FluentUI.AspNetCore.McpServer.csproj | 24 +++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj b/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj index 349b691cbe..3325b4e8d8 100644 --- a/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj +++ b/src/Tools/McpServer/Microsoft.FluentUI.AspNetCore.McpServer.csproj @@ -1,4 +1,4 @@ - + Exe @@ -42,6 +42,9 @@ $(MSBuildThisFileDirectory)all-icons.json + + + $(MSBuildThisFileDirectory)chart-comments.json @@ -93,6 +96,13 @@ + + + + + + + @@ -149,11 +159,13 @@ This runs DocApiGen to extract documentation from the FluentUI Components assembly and creates a JSON file that is then embedded as a resource. --> - + $(MSBuildThisFileDirectory)..\..\..\examples\Tools\FluentUI.Demo.DocApiGen\FluentUI.Demo.DocApiGen.csproj $(MSBuildThisFileDirectory)..\..\Core\bin\$(Configuration)\$(NetVersion)\Microsoft.FluentUI.AspNetCore.Components.dll $(MSBuildThisFileDirectory)..\..\Core\bin\$(Configuration)\$(NetVersion)\Microsoft.FluentUI.AspNetCore.Components.xml + $(MSBuildThisFileDirectory)..\..\Charts\bin\$(Configuration)\$(NetVersion)\Microsoft.FluentUI.AspNetCore.Components.Charts.dll + $(MSBuildThisFileDirectory)..\..\Charts\bin\$(Configuration)\$(NetVersion)\Microsoft.FluentUI.AspNetCore.Components.Charts.xml @@ -174,6 +186,11 @@ + + + + + - - @@ -166,9 +131,5 @@ CS1591 - - - - - + diff --git a/src/Charts/Models/CalloutAccessibilityData.cs b/src/Charts/Models/CalloutAccessibilityData.cs new file mode 100644 index 0000000000..7a0ef78796 --- /dev/null +++ b/src/Charts/Models/CalloutAccessibilityData.cs @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Accessibility data attached to a chart tooltip callout element. +/// Maps to the web component's AccessibilityData interface. +/// +public sealed record CalloutAccessibilityData +{ + /// + /// Gets the accessible label announced by screen readers for the callout element. + /// + [JsonPropertyName("ariaLabel")] + public string? AriaLabel { get; init; } +} + diff --git a/src/Charts/Models/CartesianTooltipContext.cs b/src/Charts/Models/CartesianTooltipContext.cs index 2f93438eff..b2e4ad393b 100644 --- a/src/Charts/Models/CartesianTooltipContext.cs +++ b/src/Charts/Models/CartesianTooltipContext.cs @@ -22,17 +22,17 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Charts; /// public sealed class CartesianTooltipContext : TooltipContext { - /// - /// Gets the start of the x-axis range for the hovered data element. - /// For this is an ISO 8601 date string (e.g. - /// "2009-01-01T00:00:00.000Z"). Empty for other chart types. - /// - public string? XStart { get; init; } + /// + /// Gets the start of the x-axis range for the hovered data element. + /// For this is an ISO 8601 date string (e.g. + /// "2009-01-01T00:00:00.000Z"). Empty for other chart types. + /// + public string? XStart { get; init; } - /// - /// Gets the end of the x-axis range for the hovered data element. - /// For this is an ISO 8601 date string (e.g. - /// "2009-02-28T00:00:00.000Z"). Empty for other chart types. - /// - public string? XEnd { get; init; } + /// + /// Gets the end of the x-axis range for the hovered data element. + /// For this is an ISO 8601 date string (e.g. + /// "2009-02-28T00:00:00.000Z"). Empty for other chart types. + /// + public string? XEnd { get; init; } } diff --git a/src/Charts/Models/ChartAxisValue.cs b/src/Charts/Models/ChartAxisValue.cs index 5c8668f1d9..8070aecbcf 100644 --- a/src/Charts/Models/ChartAxisValue.cs +++ b/src/Charts/Models/ChartAxisValue.cs @@ -2,13 +2,10 @@ // This file is licensed to you under the MIT License. // ------------------------------------------------------------------------ -using System.Globalization; -using System.Text.Json; using System.Text.Json.Serialization; namespace Microsoft.FluentUI.AspNetCore.Components.Charts; -#pragma warning disable MA0048 // File name must match type name /// /// Represents a value that can be plotted on any axis of any chart type. /// Holds either a numeric value (e.g. a Unix timestamp in milliseconds or an @@ -77,37 +74,3 @@ public static implicit operator ChartAxisValue(DateTime value) => /// Returns when the values are not equal. public static bool operator !=(ChartAxisValue left, ChartAxisValue right) => !left.Equals(right); } - -/// -/// Serializes and deserializes a as either a JSON -/// number or an ISO 8601 string, matching the format expected by the chart web components. -/// -internal sealed class ChartAxisValueJsonConverter : JsonConverter -{ - /// - public override ChartAxisValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) - { - if (reader.TokenType == JsonTokenType.Number) - { - return (ChartAxisValue)reader.GetDouble(); - } - - var str = reader.GetString(); - return (ChartAxisValue)DateTimeOffset.Parse(str!, CultureInfo.InvariantCulture); - } - - /// - public override void Write(Utf8JsonWriter writer, ChartAxisValue value, JsonSerializerOptions options) - { - if (value.IsDate) - { - writer.WriteStringValue(value.DateValue.ToString("O", CultureInfo.InvariantCulture)); - } - else - { - writer.WriteNumberValue(value.NumberValue); - } - } -} - -#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/ChartAxisValueJsonConverter.cs b/src/Charts/Models/ChartAxisValueJsonConverter.cs new file mode 100644 index 0000000000..14f8a363ff --- /dev/null +++ b/src/Charts/Models/ChartAxisValueJsonConverter.cs @@ -0,0 +1,43 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Serializes and deserializes a as either a JSON +/// number or an ISO 8601 string, matching the format expected by the chart web components. +/// +internal sealed class ChartAxisValueJsonConverter : JsonConverter +{ + /// + public override ChartAxisValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType == JsonTokenType.Number) + { + return (ChartAxisValue)reader.GetDouble(); + } + + var str = reader.GetString(); + return (ChartAxisValue)DateTimeOffset.Parse(str!, CultureInfo.InvariantCulture); + } + + /// + public override void Write(Utf8JsonWriter writer, ChartAxisValue value, JsonSerializerOptions options) + { + if (value.IsDate) + { + writer.WriteStringValue(value.DateValue.ToString("O", CultureInfo.InvariantCulture)); + } + else + { + writer.WriteNumberValue(value.NumberValue); + } + } +} + +#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/DonutChartModels.cs b/src/Charts/Models/DonutChart/DonutDataPoint.cs similarity index 92% rename from src/Charts/Models/DonutChartModels.cs rename to src/Charts/Models/DonutChart/DonutDataPoint.cs index a8b8af7eee..dfbb1379a4 100644 --- a/src/Charts/Models/DonutChartModels.cs +++ b/src/Charts/Models/DonutChart/DonutDataPoint.cs @@ -7,8 +7,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Charts; -#pragma warning disable MA0048 // File name must match type name - /// /// Represents a single data point in a donut chart. /// @@ -45,7 +43,7 @@ public sealed record DonutDataPoint /// /// Gets the serialized color value sent to the web component. /// Returns when is , - /// otherwise the palette token string, or null when no color is set. + /// otherwise the palette token string, or when no color is set. /// [JsonPropertyName("color")] [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] @@ -70,4 +68,3 @@ public sealed record DonutDataPoint [JsonPropertyName("yAxisCalloutData")] public string? YAxisCalloutData { get; init; } } -#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/FunnelChart/FunnelDataPoint.cs b/src/Charts/Models/FunnelChart/FunnelDataPoint.cs new file mode 100644 index 0000000000..0eaf701751 --- /dev/null +++ b/src/Charts/Models/FunnelChart/FunnelDataPoint.cs @@ -0,0 +1,57 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents a single data point in a funnel chart. +/// +public sealed record FunnelDataPoint +{ + /// + /// Gets the legend text shown for the funnel segment. + /// + [JsonPropertyName("stage")] + public string Stage { get; init; } = string.Empty; + + /// + /// Gets the numeric value of the funnel segment. + /// + [JsonPropertyName("value")] + public double Value { get; init; } + + /// + /// Gets the color used to render the funnel segment and legend. + /// Use and set to supply + /// an exact hex or CSS color string. If not provided, the web component falls back to + /// its default palette. + /// + [JsonIgnore] + public DataVizPalette? Color { get; init; } + + /// + /// Custom color value used when is . + /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. + /// + [JsonIgnore] + public string? CustomColor { get; init; } + + /// + /// Gets the serialized color value sent to the web component. + /// Returns when is , + /// otherwise the palette token string, or when no color is set. + /// + [JsonPropertyName("color")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); + + /// + /// Gets the sub-values for a stacked funnel stage. + /// + [JsonPropertyName("subValues")] + public IReadOnlyList SubValues { get; init; } = []; +} diff --git a/src/Charts/Models/FunnelChart/FunnelSubValue.cs b/src/Charts/Models/FunnelChart/FunnelSubValue.cs new file mode 100644 index 0000000000..8f6fccc70a --- /dev/null +++ b/src/Charts/Models/FunnelChart/FunnelSubValue.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents a single sub value within a stacked funnel stage +/// +public sealed record FunnelSubValue +{ + /// + /// Category name for the sub value + /// + [JsonPropertyName("category")] + public string Category { get; init; } = string.Empty; + + /// + /// Numeric value for the sub value + /// + [JsonPropertyName("value")] + public double Value { get; init; } + + /// + /// Fill color palette token for the sub value. Use + /// and set to supply an exact hex or CSS color string. + /// + [JsonIgnore] + public DataVizPalette? Color { get; init; } + + /// + /// Custom color value used when is . + /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. + /// + [JsonIgnore] + public string? CustomColor { get; init; } + + /// + /// Gets the serialized color value sent to the web component. + /// Returns when is , + /// otherwise the palette token string, or when no color is set. + /// + [JsonPropertyName("color")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); +} diff --git a/src/Charts/Models/FunnelChartModels.cs b/src/Charts/Models/FunnelChartModels.cs deleted file mode 100644 index c0c8da0147..0000000000 --- a/src/Charts/Models/FunnelChartModels.cs +++ /dev/null @@ -1,100 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using System.Text.Json.Serialization; -using Microsoft.FluentUI.AspNetCore.Components.Extensions; - -namespace Microsoft.FluentUI.AspNetCore.Components.Charts; - -#pragma warning disable MA0048 // File name must match type name - -/// -/// Represents a single sub value within a stacked funnel stage -/// -public sealed record FunnelSubValue -{ - /// - /// Category name for the sub value - /// - [JsonPropertyName("category")] - public string Category { get; init; } = string.Empty; - - /// - /// Numeric value for the sub value - /// - [JsonPropertyName("value")] - public double Value { get; init; } - - /// - /// Fill color palette token for the sub value. Use - /// and set to supply an exact hex or CSS color string. - /// - [JsonIgnore] - public DataVizPalette? Color { get; init; } - - /// - /// Custom color value used when is . - /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. - /// - [JsonIgnore] - public string? CustomColor { get; init; } - - /// - /// Gets the serialized color value sent to the web component. - /// Returns when is , - /// otherwise the palette token string, or null when no color is set. - /// - [JsonPropertyName("color")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); -} - -/// -/// Represents a single data point in a funnel chart. -/// -public sealed record FunnelDataPoint -{ - /// - /// Gets the legend text shown for the funnel segment. - /// - [JsonPropertyName("stage")] - public string Stage { get; init; } = string.Empty; - - /// - /// Gets the numeric value of the funnel segment. - /// - [JsonPropertyName("value")] - public double Value { get; init; } - - /// - /// Gets the color used to render the funnel segment and legend. - /// Use and set to supply - /// an exact hex or CSS color string. If not provided, the web component falls back to - /// its default palette. - /// - [JsonIgnore] - public DataVizPalette? Color { get; init; } - - /// - /// Custom color value used when is . - /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. - /// - [JsonIgnore] - public string? CustomColor { get; init; } - - /// - /// Gets the serialized color value sent to the web component. - /// Returns when is , - /// otherwise the palette token string, or null when no color is set. - /// - [JsonPropertyName("color")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); - - /// - /// Gets the sub-values for a stacked funnel stage. - /// - [JsonPropertyName("subValues")] - public IReadOnlyList SubValues { get; init; } = []; -} diff --git a/src/Charts/Models/GanttChartModels.cs b/src/Charts/Models/GanttChart/GanttChartDataPoint.cs similarity index 78% rename from src/Charts/Models/GanttChartModels.cs rename to src/Charts/Models/GanttChart/GanttChartDataPoint.cs index a4a09452de..1e7e677935 100644 --- a/src/Charts/Models/GanttChartModels.cs +++ b/src/Charts/Models/GanttChart/GanttChartDataPoint.cs @@ -7,31 +7,6 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Charts; -#pragma warning disable MA0048 // File name must match type name - -/// -/// Represents the x-axis range of a . -/// Both and accept numeric values -/// (e.g. Unix timestamps in milliseconds) as well as or -/// values that are serialized as ISO 8601 strings. -/// -public sealed record GanttChartXRange -{ - /// - /// Gets the start value of the range along the x-axis. - /// Assign a , , or . - /// - [JsonPropertyName("start")] - public ChartAxisValue Start { get; init; } - - /// - /// Gets the end value of the range along the x-axis. - /// Assign a , , or . - /// - [JsonPropertyName("end")] - public ChartAxisValue End { get; init; } -} - /// /// Represents a single data point in a Gantt chart. /// @@ -115,5 +90,3 @@ public sealed record GanttChartDataPoint [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public CalloutAccessibilityData? CallOutAccessibilityData { get; init; } } - -#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/GanttChart/GanttChartXRange.cs b/src/Charts/Models/GanttChart/GanttChartXRange.cs new file mode 100644 index 0000000000..a16dd8cdd6 --- /dev/null +++ b/src/Charts/Models/GanttChart/GanttChartXRange.cs @@ -0,0 +1,33 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; +#pragma warning disable MA0048 // File name must match type name + +/// +/// Represents the x-axis range of a . +/// Both and accept numeric values +/// (e.g. Unix timestamps in milliseconds) as well as or +/// values that are serialized as ISO 8601 strings. +/// +public sealed record GanttChartXRange +{ + /// + /// Gets the start value of the range along the x-axis. + /// Assign a , , or . + /// + [JsonPropertyName("start")] + public ChartAxisValue Start { get; init; } + + /// + /// Gets the end value of the range along the x-axis. + /// Assign a , , or . + /// + [JsonPropertyName("end")] + public ChartAxisValue End { get; init; } +} + +#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/HorizontalBarChart/HorizontalBarChartDataPoint.cs b/src/Charts/Models/HorizontalBarChart/HorizontalBarChartDataPoint.cs new file mode 100644 index 0000000000..f2ba17a797 --- /dev/null +++ b/src/Charts/Models/HorizontalBarChart/HorizontalBarChartDataPoint.cs @@ -0,0 +1,65 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents a single data point in a horizontal bar chart series. +/// +public sealed record HorizontalBarChartDataPoint +{ + /// + /// Gets the legend text shown for the bar segment. + /// + [JsonPropertyName("legend")] + public string Legend { get; init; } = string.Empty; + + /// + /// Gets the numeric value represented by the bar segment. + /// + [JsonPropertyName("data")] + public double Data { get; init; } + + /// + /// Gets the optional total bar length used for ratio-style rendering. + /// + [JsonPropertyName("total")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public double? Total { get; init; } + + /// + /// Gets the solid color used to render the bar segment. + /// Use and set to supply + /// an exact hex or CSS color string. If not provided, the component falls back to its + /// default palette. + /// + [JsonIgnore] + public DataVizPalette? Color { get; init; } + + /// + /// Custom color value used when is . + /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. + /// + [JsonIgnore] + public string? CustomColor { get; init; } + + /// + /// Gets the serialized color value sent to the web component. + /// Returns when is , + /// otherwise the palette token string, or when no color is set. + /// + [JsonPropertyName("color")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); + + /// + /// Gets the optional two-color gradient used to render the bar segment. + /// The array should contain exactly two color values: start and end. + /// + [JsonPropertyName("gradient")] + public string[]? Gradient { get; init; } +} diff --git a/src/Charts/Models/HorizontalBarChart/HorizontalBarChartSeries.cs b/src/Charts/Models/HorizontalBarChart/HorizontalBarChartSeries.cs new file mode 100644 index 0000000000..006cddb97e --- /dev/null +++ b/src/Charts/Models/HorizontalBarChart/HorizontalBarChartSeries.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents one horizontal bar chart series in the data payload. +/// +public sealed record HorizontalBarChartSeries +{ + /// + /// Gets the optional title shown for the data series. + /// + [JsonPropertyName("chartSeriesTitle")] + public string? ChartSeriesTitle { get; init; } + + /// + /// Gets the collection of data points rendered within the series. + /// + [JsonPropertyName("chartData")] + public IReadOnlyList ChartData { get; init; } = []; + + /// + /// Gets the optional benchmark value used to render the benchmark indicator. + /// + [JsonPropertyName("benchmarkData")] + public double? BenchmarkData { get; init; } + + /// + /// Gets optional text displayed alongside the chart data for the series. + /// + [JsonPropertyName("chartDataText")] + public string? ChartDataText { get; init; } +} diff --git a/src/Charts/Models/HorizontalBarChartModels.cs b/src/Charts/Models/HorizontalBarChartModels.cs deleted file mode 100644 index fc651c9b5e..0000000000 --- a/src/Charts/Models/HorizontalBarChartModels.cs +++ /dev/null @@ -1,98 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using System.Text.Json.Serialization; -using Microsoft.FluentUI.AspNetCore.Components.Extensions; - -namespace Microsoft.FluentUI.AspNetCore.Components.Charts; - -#pragma warning disable MA0048 // File name must match type name - -/// -/// Represents a single data point in a horizontal bar chart series. -/// -public sealed record HorizontalBarChartDataPoint -{ - /// - /// Gets the legend text shown for the bar segment. - /// - [JsonPropertyName("legend")] - public string Legend { get; init; } = string.Empty; - - /// - /// Gets the numeric value represented by the bar segment. - /// - [JsonPropertyName("data")] - public double Data { get; init; } - - /// - /// Gets the optional total bar length used for ratio-style rendering. - /// - [JsonPropertyName("total")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public double? Total { get; init; } - - /// - /// Gets the solid color used to render the bar segment. - /// Use and set to supply - /// an exact hex or CSS color string. If not provided, the component falls back to its - /// default palette. - /// - [JsonIgnore] - public DataVizPalette? Color { get; init; } - - /// - /// Custom color value used when is . - /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. - /// - [JsonIgnore] - public string? CustomColor { get; init; } - - /// - /// Gets the serialized color value sent to the web component. - /// Returns when is , - /// otherwise the palette token string, or null when no color is set. - /// - [JsonPropertyName("color")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); - - /// - /// Gets the optional two-color gradient used to render the bar segment. - /// The array should contain exactly two color values: start and end. - /// - [JsonPropertyName("gradient")] - public string[]? Gradient { get; init; } -} - -/// -/// Represents one horizontal bar chart series in the data payload. -/// -public sealed record HorizontalBarChartSeries -{ - /// - /// Gets the optional title shown for the data series. - /// - [JsonPropertyName("chartSeriesTitle")] - public string? ChartSeriesTitle { get; init; } - - /// - /// Gets the collection of data points rendered within the series. - /// - [JsonPropertyName("chartData")] - public IReadOnlyList ChartData { get; init; } = []; - - /// - /// Gets the optional benchmark value used to render the benchmark indicator. - /// - [JsonPropertyName("benchmarkData")] - public double? BenchmarkData { get; init; } - - /// - /// Gets optional text displayed alongside the chart data for the series. - /// - [JsonPropertyName("chartDataText")] - public string? ChartDataText { get; init; } -} -#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDataPoint.cs b/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDataPoint.cs new file mode 100644 index 0000000000..c44916eb28 --- /dev/null +++ b/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisDataPoint.cs @@ -0,0 +1,87 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; +using Microsoft.FluentUI.AspNetCore.Components.Extensions; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents a single data point in a horizontal bar chart series. +/// +public sealed record HorizontalBarChartWithAxisDataPoint +{ + /// + /// Gets the numeric value of the bar segment, which determines its length along the x-axis. + /// + [JsonPropertyName("x")] + public double X { get; init; } + + /// + /// Gets the category or label of the bar segment, which determines its position along the y-axis. + /// + [JsonPropertyName("y")] + public string Y { get; init; } = string.Empty; + + /// + /// Gets the legend text shown for the bar segment. + /// + [JsonPropertyName("legend")] + public string Legend { get; init; } = string.Empty; + + /// + /// Gets the solid color used to render the bar segment. + /// Use and set to supply + /// an exact hex or CSS color string. If not provided, the component falls back to its + /// default palette. + /// + [JsonIgnore] + public DataVizPalette? Color { get; init; } + + /// + /// Custom color value used when is . + /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. + /// + [JsonIgnore] + public string? CustomColor { get; init; } + + /// + /// Gets the serialized color value sent to the web component. + /// Returns when is , + /// otherwise the palette token string, or when no color is set. + /// + [JsonPropertyName("color")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); + + /// + /// Gets the optional two-color gradient used to render the bar segment. + /// The array should contain exactly two color values: start and end. + /// + [JsonPropertyName("gradient")] + public string[]? Gradient { get; init; } + + /// + /// Gets the optional accessibility data for the tooltip callout. + /// When set, is used as the accessible label + /// for the bar's callout element. + /// + [JsonPropertyName("callOutAccessibilityData")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + public CalloutAccessibilityData? CallOutAccessibilityData { get; init; } + + /// + /// Gets optional callout data for the x-axis portion of the tooltip. + /// + [JsonPropertyName("xAxisCalloutData")] + public string? XAxisCalloutData { get; init; } + + /// + /// Gets optional callout data for the y-axis portion of the tooltip. + /// If not provided, the component may fall back to the numeric data value. + /// + [JsonPropertyName("yAxisCalloutData")] + public string? YAxisCalloutData { get; init; } +} + diff --git a/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSeries.cs b/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSeries.cs new file mode 100644 index 0000000000..479216738e --- /dev/null +++ b/src/Charts/Models/HorizontalBarChartWithAxis/HorizontalBarChartWithAxisSeries.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// This file is licensed to you under the MIT License. +// ------------------------------------------------------------------------ + +using System.Text.Json.Serialization; + +namespace Microsoft.FluentUI.AspNetCore.Components.Charts; + +/// +/// Represents one horizontal bar chart series in the data payload. +/// +public sealed record HorizontalBarChartWithAxisSeries +{ + /// + /// Gets the optional title shown for the data series. + /// + [JsonPropertyName("chartSeriesTitle")] + public string? ChartSeriesTitle { get; init; } + + /// + /// Gets the collection of data points rendered within the series. + /// + [JsonPropertyName("chartData")] + public IReadOnlyList ChartData { get; init; } = []; + + /// + /// Gets the optional benchmark value used to render the benchmark indicator. + /// + [JsonPropertyName("benchmarkData")] + public double? BenchmarkData { get; init; } + + /// + /// Gets optional text displayed alongside the chart data for the series. + /// + [JsonPropertyName("chartDataText")] + public string? ChartDataText { get; init; } +} diff --git a/src/Charts/Models/HorizontalBarChartWithAxisModels.cs b/src/Charts/Models/HorizontalBarChartWithAxisModels.cs deleted file mode 100644 index 2cc8e0c24c..0000000000 --- a/src/Charts/Models/HorizontalBarChartWithAxisModels.cs +++ /dev/null @@ -1,132 +0,0 @@ -// ------------------------------------------------------------------------ -// This file is licensed to you under the MIT License. -// ------------------------------------------------------------------------ - -using System.Text.Json.Serialization; -using Microsoft.FluentUI.AspNetCore.Components.Extensions; - -namespace Microsoft.FluentUI.AspNetCore.Components.Charts; - -#pragma warning disable MA0048 // File name must match type name - -/// -/// Represents a single data point in a horizontal bar chart series. -/// -public sealed record HorizontalBarChartWithAxisDataPoint -{ - /// - /// Gets the numeric value of the bar segment, which determines its length along the x-axis. - /// - [JsonPropertyName("x")] - public double X { get; init; } - - /// - /// Gets the category or label of the bar segment, which determines its position along the y-axis. - /// - [JsonPropertyName("y")] - public string Y { get; init; } = string.Empty; - - /// - /// Gets the legend text shown for the bar segment. - /// - [JsonPropertyName("legend")] - public string Legend { get; init; } = string.Empty; - - /// - /// Gets the solid color used to render the bar segment. - /// Use and set to supply - /// an exact hex or CSS color string. If not provided, the component falls back to its - /// default palette. - /// - [JsonIgnore] - public DataVizPalette? Color { get; init; } - - /// - /// Custom color value used when is . - /// Accepts an HTML hex color string (e.g. #0099BC) or a CSS variable. - /// - [JsonIgnore] - public string? CustomColor { get; init; } - - /// - /// Gets the serialized color value sent to the web component. - /// Returns when is , - /// otherwise the palette token string, or null when no color is set. - /// - [JsonPropertyName("color")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public string? SerializedColor => Color == DataVizPalette.Custom ? CustomColor : Color?.ToAttributeValue(); - - /// - /// Gets the optional two-color gradient used to render the bar segment. - /// The array should contain exactly two color values: start and end. - /// - [JsonPropertyName("gradient")] - public string[]? Gradient { get; init; } - - /// - /// Gets the optional accessibility data for the tooltip callout. - /// When set, is used as the accessible label - /// for the bar's callout element. - /// - [JsonPropertyName("callOutAccessibilityData")] - [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] - public CalloutAccessibilityData? CallOutAccessibilityData { get; init; } - - /// - /// Gets optional callout data for the x-axis portion of the tooltip. - /// - [JsonPropertyName("xAxisCalloutData")] - public string? XAxisCalloutData { get; init; } - - /// - /// Gets optional callout data for the y-axis portion of the tooltip. - /// If not provided, the component may fall back to the numeric data value. - /// - [JsonPropertyName("yAxisCalloutData")] - public string? YAxisCalloutData { get; init; } -} - -/// -/// Accessibility data attached to a chart tooltip callout element. -/// Maps to the web component's AccessibilityData interface. -/// -public sealed record CalloutAccessibilityData -{ - /// - /// Gets the accessible label announced by screen readers for the callout element. - /// - [JsonPropertyName("ariaLabel")] - public string? AriaLabel { get; init; } -} - -/// -/// Represents one horizontal bar chart series in the data payload. -/// -public sealed record HorizontalBarChartSeriesWithAxis -{ - /// - /// Gets the optional title shown for the data series. - /// - [JsonPropertyName("chartSeriesTitle")] - public string? ChartSeriesTitle { get; init; } - - /// - /// Gets the collection of data points rendered within the series. - /// - [JsonPropertyName("chartData")] - public IReadOnlyList ChartData { get; init; } = []; - - /// - /// Gets the optional benchmark value used to render the benchmark indicator. - /// - [JsonPropertyName("benchmarkData")] - public double? BenchmarkData { get; init; } - - /// - /// Gets optional text displayed alongside the chart data for the series. - /// - [JsonPropertyName("chartDataText")] - public string? ChartDataText { get; init; } -} -#pragma warning restore MA0048 // File name must match type name diff --git a/src/Charts/Models/TooltipContext.cs b/src/Charts/Models/TooltipContext.cs index 96ccf6bf41..d8af8a7407 100644 --- a/src/Charts/Models/TooltipContext.cs +++ b/src/Charts/Models/TooltipContext.cs @@ -12,40 +12,40 @@ namespace Microsoft.FluentUI.AspNetCore.Components.Charts; /// public class TooltipContext { - /// - /// Gets the legend label associated with the hovered data element. - /// - public string? Legend { get; init; } + /// + /// Gets the legend label associated with the hovered data element. + /// + public string? Legend { get; init; } - /// - /// Gets the primary Y-axis (or value) formatted string for the hovered data element. - /// - public string? YValue { get; init; } + /// + /// Gets the primary Y-axis (or value) formatted string for the hovered data element. + /// + public string? YValue { get; init; } - /// - /// Gets the secondary X-axis (or range) formatted string for the hovered data element. - /// Only populated for axis-based charts (Gantt, HorizontalBarChartWithAxis). - /// - public string? XValue { get; init; } + /// + /// Gets the secondary X-axis (or range) formatted string for the hovered data element. + /// Only populated for axis-based charts (Gantt, HorizontalBarChartWithAxis). + /// + public string? XValue { get; init; } - /// - /// Gets the color associated with the hovered data element, - /// or when the data point uses a custom color. - /// Use with - /// as the fallback to resolve the displayed color in both cases. - /// - public DataVizPalette? Color { get; init; } + /// + /// Gets the color associated with the hovered data element, + /// or when the data point uses a custom color. + /// Use with + /// as the fallback to resolve the displayed color in both cases. + /// + public DataVizPalette? Color { get; init; } - /// - /// Gets the raw CSS color string (e.g. #ff0000) when the data point uses - /// . when is set. - /// - public string? CustomColor { get; init; } + /// + /// Gets the raw CSS color string (e.g. #ff0000) when the data point uses + /// . when is set. + /// + public string? CustomColor { get; init; } - /// - /// Gets the raw JSON object serialized from the JavaScript data point. - /// Use this when you need to access chart-specific properties not covered - /// by the typed members above. - /// - public string? RawJson { get; init; } + /// + /// Gets the raw JSON object serialized from the JavaScript data point. + /// Use this when you need to access chart-specific properties not covered + /// by the typed members above. + /// + public string? RawJson { get; init; } } diff --git a/src/Charts/README.md b/src/Charts/README.md index c82e98439d..4e0b8c800c 100644 --- a/src/Charts/README.md +++ b/src/Charts/README.md @@ -1 +1,66 @@ -# Welcome to the Microsoft Fluent UI Chart Components for Blazor +# Microsoft Fluent UI Chart Components for Blazor + +[![NuGet](https://img.shields.io/nuget/v/Microsoft.FluentUI.AspNetCore.Components.Charts?label=NuGet%20Charts)](https://www.nuget.org/packages/Microsoft.FluentUI.AspNetCore.Components.Charts) + +## About + +This package provides Blazor chart components that bring the Fluent Design System's charting capabilities to .NET Blazor applications. The components are based on the [Fluent UI V9 React Charts package (`@fluentui/react-charts`)](https://react.fluentui.dev/?path=/docs/charts-introduction--docs) and offer the same look, feel, and behavior as their React counterparts. + +> **Note:** Currently, only a subset of the chart types available in `@fluentui/react-charts` are implemented. Additional chart types will be added in future releases. + +### Available chart types + +| Chart type | Blazor component | +|---|---| +| Donut Chart | `` | +| Funnel Chart | `` | +| Gantt Chart | `` | +| Horizontal Bar Chart | `` | +| Horizontal Bar Chart with Axis | `` | + +### Coming soon + +The following chart types from the React Charts package are planned for future releases: + +- Area Chart +- Gauge Chart +- Grouped Vertical Bar Chart +- Heat Map Chart +- Line Chart +- Polar Chart +- Sankey Chart +- Scatter Chart +- Sparkline +- Vertical Bar Chart +- Vertical Stacked Bar Chart +- And more... + +## Prerequisites + +This package is an **extension** to the [Microsoft Fluent UI Blazor library](https://www.nuget.org/packages/Microsoft.FluentUI.AspNetCore.Components) and **cannot be used as a standalone package**. The main `Microsoft.FluentUI.AspNetCore.Components` package must be installed and configured in your project first. + +For instructions on how to set up the Fluent UI Blazor library, see the [Getting Started](https://www.fluentui-blazor.net/GetStarted) documentation. + +## Installation + +Install the package using the .NET CLI: + +```shell +dotnet add package Microsoft.FluentUI.AspNetCore.Components.Charts +``` + +Or add the package using the NuGet Package Manager in Visual Studio. + +> No additional setup required. Once the package is installed, the Core library will automatically detect and register everything it needs. There is no need to manually add services or script references. + +## Samples and documentation + +Live samples and full documentation for all chart components are available on the demo site: + +[https://v5.fluentui-blazor.net](https://v5.fluentui-blazor.net) + +## Support + +The Microsoft Fluent UI Blazor library is an open source project and is **not** an official part of ASP.NET Core, which means it's **not** officially +supported and isn't committed to ship updates as part of any official .NET updates. It is built and maintained by Microsoft employees (**and** other contributors) +and offers support, like most other open source projects, on a best effort base through the GitHub repository **only**. diff --git a/src/Charts/DataVizPaletteJsonConverter.cs b/src/Charts/Serialization/DataVizPaletteJsonConverter.cs similarity index 100% rename from src/Charts/DataVizPaletteJsonConverter.cs rename to src/Charts/Serialization/DataVizPaletteJsonConverter.cs diff --git a/src/Charts/Models/DonutChartDataJsonSerializerContext.cs b/src/Charts/Serialization/DonutChartDataJsonSerializerContext.cs similarity index 100% rename from src/Charts/Models/DonutChartDataJsonSerializerContext.cs rename to src/Charts/Serialization/DonutChartDataJsonSerializerContext.cs diff --git a/src/Charts/Models/FunnelChartDataJsonSerializerContext.cs b/src/Charts/Serialization/FunnelChartDataJsonSerializerContext.cs similarity index 100% rename from src/Charts/Models/FunnelChartDataJsonSerializerContext.cs rename to src/Charts/Serialization/FunnelChartDataJsonSerializerContext.cs diff --git a/src/Charts/Models/GanttChartDataJsonSerializerContext.cs b/src/Charts/Serialization/GanttChartDataJsonSerializerContext.cs similarity index 100% rename from src/Charts/Models/GanttChartDataJsonSerializerContext.cs rename to src/Charts/Serialization/GanttChartDataJsonSerializerContext.cs diff --git a/src/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs b/src/Charts/Serialization/HorizontalBarChartDataJsonSerializerContext.cs similarity index 100% rename from src/Charts/Models/HorizontalBarChartDataJsonSerializerContext.cs rename to src/Charts/Serialization/HorizontalBarChartDataJsonSerializerContext.cs diff --git a/src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs b/src/Charts/Serialization/HorizontalBarChartWithAxisDataJsonSerializerContext.cs similarity index 100% rename from src/Charts/Models/HorizontalBarChartWithAxisDataJsonSerializerContext.cs rename to src/Charts/Serialization/HorizontalBarChartWithAxisDataJsonSerializerContext.cs diff --git a/src/Charts/tsconfig.json b/src/Charts/tsconfig.json new file mode 100644 index 0000000000..58552a4fa3 --- /dev/null +++ b/src/Charts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig to read more about this file */ + + /* Language and Environment */ + "target": "ESNext", + + /* Modules */ + "module": "ESNext", + + /* Create source map files for emitted JavaScript files. */ + "sourceMap": false, + + /* Type Checking */ + "strict": true, + "noImplicitAny": true, + "noEmitOnError": true, + "removeComments": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true + }, + "include": [ + "wwwroot/js/**/*.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/src/Charts/wwwroot/js/chart-tooltip-bridge.ts b/src/Charts/wwwroot/js/chart-tooltip-bridge.ts new file mode 100644 index 0000000000..03226f617e --- /dev/null +++ b/src/Charts/wwwroot/js/chart-tooltip-bridge.ts @@ -0,0 +1,218 @@ +// chart-tooltip-bridge.ts +// ES module that wires a Blazor-rendered tooltip portal into the chart web component's +// shadow DOM by supplying a `tooltipRenderer` function on the custom element. +// +// Flow: +// 1. Blazor calls `initTooltipBridge(chartId, portalId, dotNetRef)` in OnAfterRenderAsync. +// 2. This module waits for the custom element to be defined, then: +// a. Sets up a MutationObserver on the hidden portal div so that whenever Blazor +// re-renders the template the updated content is pushed into shadow DOM. +// b. Sets `tooltipRenderer` on the element instance. +// 3. When the chart fires the renderer callback (on hover): +// a. We notify Blazor fire-and-forget via `UpdateTooltipContextAsync` so it updates +// the portal div with the hovered data point. +// b. We return the default tooltip HTML immediately so the tooltip appears at once. +// c. Once Blazor's render completes the MutationObserver fires and swaps in the +// custom Blazor-rendered content automatically. +// 4. `destroyTooltipBridge` removes the renderer, disconnects the observer, and cleans up. + +interface Bridge { + element: HTMLElement & { tooltipRenderer?: TooltipRenderer }; + dotNetRef: any; + observer: MutationObserver; + shadowObserver: MutationObserver; +} + +type TooltipRenderer = (dataPoint: unknown, defaultRender: (dataPoint: unknown) => string) => string; + +const _bridges = new Map(); + +/** + * Initialises the tooltip bridge for a single chart instance. + * @param chartId The `id` attribute of the fluent-*-chart element. + * @param portalId The `id` of the hidden Blazor portal div. + * @param dotNetRef A DotNetObjectReference to the Blazor component. + */ +export async function initTooltipBridge(chartId: string, portalId: string, dotNetRef: any): Promise { + const chartEl = document.getElementById(chartId) as (HTMLElement & { tooltipRenderer?: TooltipRenderer }) | null; + if (!chartEl) { + return; + } + + // Wait until the custom element is upgraded (defined + connected). + if (customElements.get(chartEl.tagName.toLowerCase()) === undefined) { + await customElements.whenDefined(chartEl.tagName.toLowerCase()); + } + + // Portal div where Blazor renders the custom tooltip template. + const portal = document.getElementById(portalId); + + // Stores the latest portal content clone waiting to be pushed into .tooltip-body. + // Set whenever Blazor re-renders the portal; cleared after a successful push. + // This decouples the two async events (Blazor render + FAST .tooltip-body insertion) + // so whichever arrives second can complete the push — critical for charts like + // FunnelChart where mouseout/mouseover cycles hide/show .tooltip-body rapidly. + let _pendingContent: Element | null = null; + + /** + * Push _pendingContent into .tooltip-body if both are available. + * Safe to call at any time; no-ops when either is absent or already pushed. + */ + const tryPushToTooltipBody = (): void => { + if (!_pendingContent) return; + const tooltipBody = chartEl.shadowRoot?.querySelector(".tooltip-body"); + if (!tooltipBody) return; + tooltipBody.innerHTML = ""; + tooltipBody.appendChild(_pendingContent.cloneNode(true)); + _pendingContent = null; + }; + + /** + * Called by the portal MutationObserver whenever Blazor re-renders the portal. + * Stores the updated content and immediately tries to push it to .tooltip-body. + * If .tooltip-body is not yet in the shadow DOM (FAST hasn't run its RAF yet), + * the content is kept in _pendingContent for the shadow observer to push later. + */ + const pushPortalContent = (): void => { + if (!portal?.firstElementChild) return; + _pendingContent = portal.firstElementChild.cloneNode(true) as Element; + tryPushToTooltipBody(); + }; + + // Watch the portal for any DOM change caused by Blazor re-rendering the template. + const observer = new MutationObserver(pushPortalContent); + if (portal) { + observer.observe(portal, { + childList: true, + subtree: true, + characterData: true, + }); + } + + // Watch the chart's shadow root for .tooltip-body being inserted by FAST's RAF. + // When the tooltip becomes visible, FAST inserts .tooltip-body via requestAnimationFrame. + // For charts with rapid show/hide cycles (e.g. FunnelChart), Blazor's async render may + // finish only after .tooltip-body has already been removed. The shadow observer ensures + // _pendingContent is pushed the next time .tooltip-body appears, regardless of order. + const shadowObserver = new MutationObserver(tryPushToTooltipBody); + if (chartEl.shadowRoot) { + shadowObserver.observe(chartEl.shadowRoot, { + childList: true, + subtree: true, + }); + } + + /** + * The tooltipRenderer assigned to the WC property. + * + * Returns the default tooltip HTML synchronously so the tooltip is visible + * immediately. At the same time, notifies Blazor fire-and-forget so it can + * re-render the portal with custom content. Once Blazor's OnAfterRenderAsync + * completes it calls updateTooltipContent() to swap in the custom HTML. + * + * @param dataPoint The raw JS data point object from the chart. + * @param defaultRender The chart's built-in default renderer. + * @returns Default tooltip HTML (shown immediately). + */ + const renderer: TooltipRenderer = (dataPoint: any, defaultRender): string => { + // Extract common fields; fall back to empty strings for axis charts. + const legend: string = dataPoint?.legend ?? dataPoint?.stage ?? ""; + const yValue: string = + dataPoint?.yValue ?? + dataPoint?.value ?? + dataPoint?.yAxisCalloutData ?? + (dataPoint?.data != null ? String(dataPoint.data) : null) ?? + String(dataPoint?.y ?? ""); + + // For GanttChart dataPoint.x is { start, end } — guard against [object Object]. + const rawX: unknown = dataPoint?.x; + const xIsRange = rawX !== null && typeof rawX === "object"; + const xValue: string = + dataPoint?.xValue ?? + dataPoint?.xAxisCalloutData ?? + (xIsRange ? "" : String(rawX ?? "")); + + // XStart / XEnd: ISO date strings for GanttChart ranges; empty for all other charts. + const xStart: string = xIsRange ? _toISODateString((rawX as { start: unknown }).start) : ""; + const xEnd: string = xIsRange ? _toISODateString((rawX as { end: unknown }).end) : ""; + + const color: string = dataPoint?.color ?? ""; + const rawJson: string = _safeStringify(dataPoint); + + // Fire-and-forget: tell Blazor about the new data point. + // The MutationObserver will push the updated portal content once Blazor re-renders. + dotNetRef + .invokeMethodAsync( + "UpdateTooltipContextAsync", + legend, + yValue, + xValue, + color, + rawJson, + xStart, + xEnd, + ) + .catch(() => { + /* component disposed — ignore */ + }); + + // Return default content immediately so the tooltip is shown at once + // while Blazor re-renders the portal in the background. + return defaultRender(dataPoint); + }; + + // Assign the renderer to the element property. + chartEl.tooltipRenderer = renderer; + + _bridges.set(chartId, { + element: chartEl, + dotNetRef, + observer, + shadowObserver, + }); +} + +/** + * Removes the tooltip bridge for a chart instance and disposes the DotNet reference. + * Called from `FluentChartBase.DisposeAsync`. + * @param chartId + */ +export function destroyTooltipBridge(chartId: string): void { + const bridge = _bridges.get(chartId); + if (bridge) { + bridge.element.tooltipRenderer = undefined; + bridge.observer?.disconnect(); + bridge.shadowObserver?.disconnect(); + _bridges.delete(chartId); + } +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Converts a date-like value to a string suitable for Blazor to parse. + * Strings are returned unchanged (already ISO-formatted from C# JSON serialization). + * Date objects are converted to ISO strings. Numbers are stringified. + * Returns an empty string for null/undefined. + */ +function _toISODateString(value: unknown): string { + if (value == null) return ""; + if (typeof value === "string") return value; + if (typeof value === "number") return String(value); + if (value instanceof Date) + return isNaN(value.getTime()) ? "" : value.toISOString(); + return String(value); +} + +/** + * Safely serialises a value to JSON, handling circular references. + */ +function _safeStringify(value: unknown): string { + try { + return JSON.stringify(value); + } catch { + return "{}"; + } +} diff --git a/src/Core.Scripts/.npmrc b/src/Core.Scripts/.npmrc index a8f33531f6..82b193ea84 100644 --- a/src/Core.Scripts/.npmrc +++ b/src/Core.Scripts/.npmrc @@ -1,3 +1,2 @@ registry=https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-public-npm/npm/registry/ -always-auth=true - +always-auth=false From e51129e35cedc0da5b1ef9193e7f4de9da7865c6 Mon Sep 17 00:00:00 2001 From: Vincent Baaij Date: Tue, 7 Jul 2026 19:52:25 +0200 Subject: [PATCH 50/60] Update chart script code --- .../DonutChart/DonutChartOutsideLabels.razor | 5 +- .../DonutChart/DonutChartSizing.razor | 6 +- examples/Demo/FluentUI.Demo/Program.cs | 2 + .../src/chart-legend/chart-legend.styles.ts | 82 +- .../src/chart-legend/chart-legend.template.ts | 49 +- .../src/chart-legend/chart-legend.ts | 158 +++- .../src/donut-chart/donut-chart.options.ts | 2 +- .../src/donut-chart/donut-chart.styles.ts | 1 + .../src/donut-chart/donut-chart.template.ts | 8 +- .../src/donut-chart/donut-chart.ts | 206 ++-- src/Charts.Scripts/src/donut-chart/index.ts | 1 + .../src/funnel-chart/funnel-chart.options.ts | 8 +- .../src/funnel-chart/funnel-chart.styles.ts | 1 + .../src/funnel-chart/funnel-chart.template.ts | 5 +- .../src/funnel-chart/funnel-chart.ts | 42 +- .../src/funnel-chart/funnel-geometry.ts | 10 +- src/Charts.Scripts/src/funnel-chart/index.ts | 1 + .../src/gantt-chart/gantt-chart.options.ts | 9 +- .../src/gantt-chart/gantt-chart.styles.ts | 1 + .../src/gantt-chart/gantt-chart.template.ts | 20 +- .../src/gantt-chart/gantt-chart.ts | 289 ++---- src/Charts.Scripts/src/gantt-chart/index.ts | 1 + .../horizontal-bar-chart-with-axis.options.ts | 29 +- .../horizontal-bar-chart-with-axis.styles.ts | 1 + ...horizontal-bar-chart-with-axis.template.ts | 20 +- .../horizontal-bar-chart-with-axis.ts | 248 +---- .../horizontal-bar-chart-with-axis/index.ts | 1 + .../horizontal-bar-chart.options.ts | 31 +- .../horizontal-bar-chart.styles.ts | 1 + .../horizontal-bar-chart.template.ts | 1 + .../horizontal-bar-chart.ts | 78 +- .../src/horizontal-bar-chart/index.ts | 1 + .../src/utils/cartesian-axis-helpers.ts | 34 + .../src/utils/cartesian-axis-shared.ts | 893 ++++++++++++++++++ .../src/utils/cartesian-chart-base.ts | 78 ++ src/Charts.Scripts/src/utils/chart-base.ts | 121 ++- .../{chart.options.ts => chart-options.ts} | 10 + .../src/utils/chart-tooltip.template.ts | 17 +- 38 files changed, 1877 insertions(+), 594 deletions(-) create mode 100644 src/Charts.Scripts/src/utils/cartesian-axis-helpers.ts create mode 100644 src/Charts.Scripts/src/utils/cartesian-axis-shared.ts rename src/Charts.Scripts/src/utils/{chart.options.ts => chart-options.ts} (88%) diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor index c60ceb7b2c..805536abb4 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartOutsideLabels.razor @@ -2,10 +2,7 @@ @code { diff --git a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor index f3d5a1e090..6d11d0d1fd 100644 --- a/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor +++ b/examples/Demo/FluentUI.Demo.Client/Documentation/Components/Charts/Examples/DonutChart/DonutChartSizing.razor @@ -15,9 +15,9 @@ @code { - private int _width = 320; - private int _height = 320; - private int _innerRadius = 55; + private int _width = 300; + private int _height = 300; + private int _innerRadius = 100; private readonly IReadOnlyList data = new List { diff --git a/examples/Demo/FluentUI.Demo/Program.cs b/examples/Demo/FluentUI.Demo/Program.cs index 1802348295..f6ce094e4c 100644 --- a/examples/Demo/FluentUI.Demo/Program.cs +++ b/examples/Demo/FluentUI.Demo/Program.cs @@ -4,6 +4,7 @@ using FluentUI.Demo.Client; using Microsoft.FluentUI.AspNetCore.Components; +using Microsoft.FluentUI.AspNetCore.Components.Charts; var builder = WebApplication.CreateBuilder(args); @@ -27,6 +28,7 @@ // config.DefaultValues.ForAny>().Set(p => p.Width, "100%"); // config.DefaultValues.ForAny>().Set(p => p.Multiple, false); + config.DefaultValues.ForAny().Set(p => p.RoundedCorners, true); // Use a custom localizer config.Localizer = new FluentUI.Demo.MyLocalizer(); }); diff --git a/src/Charts.Scripts/src/chart-legend/chart-legend.styles.ts b/src/Charts.Scripts/src/chart-legend/chart-legend.styles.ts index 266b0b7610..8c147a8938 100644 --- a/src/Charts.Scripts/src/chart-legend/chart-legend.styles.ts +++ b/src/Charts.Scripts/src/chart-legend/chart-legend.styles.ts @@ -1,7 +1,11 @@ import type { ElementStyles } from '@microsoft/fast-element'; import { css } from '@microsoft/fast-element'; import { + borderRadiusSmall, + borderRadiusMedium, colorNeutralForeground1, + colorStrokeFocus2, + colorSubtleBackgroundHover, spacingHorizontalL, spacingHorizontalNone, spacingHorizontalS, @@ -9,6 +13,7 @@ import { spacingVerticalNone, spacingVerticalS, strokeWidthThin, + strokeWidthThick, typographyCaption1Styles, } from '@fluentui/web-components'; @@ -21,17 +26,25 @@ export const styles: ElementStyles = css` :host { display: flex; flex-direction: row; - flex-wrap: wrap; + flex-wrap: nowrap; + overflow-x: clip; + overflow-y: visible; + position: relative; + box-sizing: border-box; padding-top: ${spacingVerticalL}; + padding-inline-start: ${spacingHorizontalS}; width: 100%; align-items: center; - margin: -${spacingVerticalS} ${spacingHorizontalNone} ${spacingVerticalNone} -${spacingHorizontalS}; } :host([hidden]) { display: none; } + :host([center]) { + justify-content: center; + } + /* ── Position overrides ──────────────────────────────────────────── */ /* top: legend sits above chart — padding moves from top to bottom */ @@ -40,10 +53,11 @@ export const styles: ElementStyles = css` padding-bottom: ${spacingVerticalL}; } - /* start: legend sits inline-start of chart */ + /* start / end: vertical column layout — restore wrapping, no overflow detection */ :host([position='start']) { flex-direction: column; - flex-wrap: nowrap; + flex-wrap: wrap; + overflow: visible; width: auto; padding-top: 0; padding-inline-end: ${spacingHorizontalL}; @@ -51,10 +65,10 @@ export const styles: ElementStyles = css` margin: 0; } - /* end: legend sits inline-end of chart */ :host([position='end']) { flex-direction: column; - flex-wrap: nowrap; + flex-wrap: wrap; + overflow: visible; width: auto; padding-top: 0; padding-inline-start: ${spacingHorizontalL}; @@ -64,12 +78,23 @@ export const styles: ElementStyles = css` .legend { display: flex; + flex-shrink: 0; align-items: center; cursor: pointer; border: none; padding: ${spacingHorizontalS}; background: none; text-transform: capitalize; + border-radius: ${borderRadiusMedium}; + } + + .legend:hover { + background-color: ${colorSubtleBackgroundHover}; + } + + .legend:focus-visible { + outline: ${strokeWidthThick} solid ${colorStrokeFocus2}; + outline-offset: 1px; } .legend-rect { @@ -79,6 +104,14 @@ export const styles: ElementStyles = css` border: ${strokeWidthThin} solid; } + .legend-rect.rounded { + border-radius: ${borderRadiusSmall}; + } + +/* Same for overflow menu items */ +fluent-menu-item .legend-rect.rounded { + border-radius: ${borderRadiusSmall}; +} .legend-text { ${typographyCaption1Styles} color: ${colorNeutralForeground1}; @@ -92,6 +125,43 @@ export const styles: ElementStyles = css` opacity: 0.67; } + /* ── Overflow menu (fluent-menu/fluent-menu-item) ───────────────── */ + + fluent-menu { + flex-shrink: 0; + } + + /* + * fluent-menu-list sets data-indent="2" on all items when any item has + * role="menuitemcheckbox", which reserves a 20px column for the checkmark. + * We suppress the checkmark visually (empty ) and + * collapse that first column to 0px so no space is wasted. + * Outer-context author styles beat :host() shadow rules in the CSS cascade. + */ + fluent-menu-item { + grid-template-columns: 0 20px auto auto; + } + + fluent-menu-item .legend-rect { + width: 12px; + height: 12px; + border: ${strokeWidthThin} solid; + } + + fluent-menu-item.inactive .legend-rect { + opacity: 0.1; + } + + fluent-menu-item.inactive .legend-text { + opacity: 0.67; + } + + fluent-menu-item .legend-text { + ${typographyCaption1Styles} + color: ${colorNeutralForeground1}; + text-transform: capitalize; + } + @media (forced-colors: active) { .legend-rect { forced-color-adjust: none; diff --git a/src/Charts.Scripts/src/chart-legend/chart-legend.template.ts b/src/Charts.Scripts/src/chart-legend/chart-legend.template.ts index 610c007d1c..7b8fa30a72 100644 --- a/src/Charts.Scripts/src/chart-legend/chart-legend.template.ts +++ b/src/Charts.Scripts/src/chart-legend/chart-legend.template.ts @@ -1,6 +1,9 @@ -import { type ElementViewTemplate, html, repeat } from '@microsoft/fast-element'; +import { type ElementViewTemplate, html, repeat, when } from '@microsoft/fast-element'; +// NOTE: Item visibility is managed imperatively in ChartLegend._measure() — not +// through FAST style bindings — because repeat() does not propagate parent +// observable changes into inner bindings reactively. import type { ChartLegend } from './chart-legend.js'; -import type { Legend } from '../utils/chart.options.js'; +import type { Legend } from '../utils/chart-options.js'; import { getColorFromToken } from '../utils/chart-helpers.js'; /** @@ -33,7 +36,7 @@ export function chartLegendTemplate(): ElementViewTemplat @keydown="${(x, c) => c.parent._handleLegendKeydown(c.event as KeyboardEvent)}" >
@@ -41,6 +44,46 @@ export function chartLegendTemplate(): ElementViewTemplat `, )} + ${when( + x => x._overflowCount > 0, + html` + + + +${x => x._overflowCount} ${x => x.overflowText ?? 'more'} + + + ${repeat( + x => x._overflowItems, + html` + + +
+ ${x => x.legend} +
+ `, + )} +
+
+ `, + )} `; } diff --git a/src/Charts.Scripts/src/chart-legend/chart-legend.ts b/src/Charts.Scripts/src/chart-legend/chart-legend.ts index 9ca86e7bd3..0e3f4250fa 100644 --- a/src/Charts.Scripts/src/chart-legend/chart-legend.ts +++ b/src/Charts.Scripts/src/chart-legend/chart-legend.ts @@ -1,5 +1,5 @@ import { FASTElement, attr, observable } from '@microsoft/fast-element'; -import type { ChartLegendPosition, Legend } from '../utils/chart.options.js'; +import type { ChartLegendPosition, Legend } from '../utils/chart-options.js'; import { jsonConverter } from '../utils/chart-helpers.js'; /** @@ -49,6 +49,42 @@ export class ChartLegend extends FASTElement { @attr public position?: ChartLegendPosition; + /** Label appended to the overflow count. Defaults to `'more'` → "+3 more". */ + @attr({ attribute: 'overflow-text' }) + public overflowText?: string; + + /** Centers the legend row horizontally within its host. */ + @attr({ attribute: 'center', mode: 'boolean' }) + public center = false; + + /** + * Enable rounded corners on legend colored boxes. + * When undefined, inherits from the parent chart's rounded-corners setting. + * When explicitly set, overrides the chart setting. + */ + @attr({ attribute: 'round-boxes', mode: 'boolean' }) + public roundBoxes?: boolean; + + /** + * Number of items that overflow the row. Drives the `when()` that renders + * the "+N more" button and the overflow popup. + */ + @observable + public _overflowCount: number = 0; + + /** + * The subset of `items` that are hidden in the row and shown in the popup. + * Updated imperatively by `_measure()` alongside DOM visibility changes. + */ + @observable + public _overflowItems: Legend[] = []; + + /** Index boundary used for imperative DOM hiding — not an observable. */ + private _visibleCount: number = Number.MAX_SAFE_INTEGER; + /** Cached offsetWidths so hidden items (offsetWidth=0) can still be measured. */ + private _itemWidths: number[] = []; + private _resizeObserver?: ResizeObserver; + connectedCallback() { // Class field initializers create own data properties that shadow the FAST // @attr and @observable reactive getter/setters on the prototype. Delete them @@ -57,8 +93,8 @@ export class ChartLegend extends FASTElement { // Save defaults first so we can restore them for fields that have no // corresponding HTML attribute (FAST won't call the setter in that case). const self = this as Record; - const attrFields = ['items', 'position'] as const; - const observableFields = ['highlighted', 'selected'] as const; + const attrFields = ['items', 'position', 'overflowText', 'center', 'roundBoxes'] as const; + const observableFields = ['highlighted', 'selected', '_overflowCount', '_overflowItems'] as const; const savedAttr: Partial> = {}; const savedObs: Partial> = {}; @@ -86,10 +122,111 @@ export class ChartLegend extends FASTElement { self[field] = savedAttr[field]; } } + + // Attach ResizeObserver — fires on every layout change of the host element. + this._resizeObserver = new ResizeObserver(() => this._measure()); + this._resizeObserver.observe(this); + + // Initial measurement after the first render frame so all item buttons + // are in the DOM and have layout (offsetWidth > 0). + requestAnimationFrame(() => this._measure()); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this._resizeObserver?.disconnect(); + this._resizeObserver = undefined; + } + + /** Called by FAST when `items` changes — reset state and re-measure. */ + itemsChanged() { + this._itemWidths = []; + this._visibleCount = Number.MAX_SAFE_INTEGER; + this._overflowCount = 0; + this._overflowItems = []; + requestAnimationFrame(() => this._measure()); + } + + /** + * Measures item widths, imperatively shows/hides individual buttons, and + * updates the `_overflowCount` / `_overflowItems` observables which control + * the "+N more" button and popup via FAST `when()` directives. + * + * This is deliberately imperative: FAST's `repeat()` does not propagate + * parent-observable changes down to inner style bindings reactively, so + * visibility is managed through direct `element.style.display` assignments. + */ + _measure() { + // Vertical positions (start / end) always stack — no overflow needed. + if (this.position === 'start' || this.position === 'end') { + return; + } + + const root = this.shadowRoot; + if (!root) return; + + const hostWidth = this.clientWidth; + if (hostWidth === 0) return; + + const buttons = Array.from(root.querySelectorAll('.legend:not(.overflow-button)')); + if (buttons.length === 0) return; + + // Make all buttons visible first so we can measure their natural widths. + for (const btn of buttons) { + btn.style.display = ''; + } + + // Build / refresh the width cache. + for (let i = 0; i < buttons.length; i++) { + const w = buttons[i].offsetWidth; + if (w > 0) this._itemWidths[i] = w; + } + + const totalWidth = this._itemWidths.slice(0, buttons.length).reduce((s, w) => s + (w ?? 0), 0); + + if (totalWidth <= hostWidth) { + // Everything fits — clear any previous overflow state. + this._visibleCount = buttons.length; + this._overflowCount = 0; + this._overflowItems = []; + return; + } + + // Reserve space for the overflow button. + // If it is already in the DOM we can measure it; otherwise use 80 px as a + // conservative estimate (the ResizeObserver will fire again after FAST adds + // the button to the DOM and the measurement will self-correct). + const overflowMenu = root.querySelector('fluent-menu'); + const overflowBtnWidth = overflowMenu ? overflowMenu.offsetWidth || 80 : 80; + + // Find how many items fit alongside the overflow button. + let used = 0; + let count = 0; + for (let i = 0; i < buttons.length; i++) { + const w = this._itemWidths[i] ?? 0; + if (used + w + overflowBtnWidth <= hostWidth) { + used += w; + count = i + 1; + } else { + break; + } + } + + // Always keep at least one item visible. + count = Math.max(count, 1); + this._visibleCount = count; + + // Imperatively hide overflow items — this is not driven through FAST bindings. + for (let i = 0; i < buttons.length; i++) { + buttons[i].style.display = i >= count ? 'none' : ''; + } + + this._overflowCount = buttons.length - count; + this._overflowItems = this.items.slice(count); } /** - * Roving tabindex handler for legend button keyboard navigation. + * Roving tabindex handler for visible legend button keyboard navigation. * Arrow keys move focus between legend items; all other keys are ignored. */ public _handleLegendKeydown(e: KeyboardEvent): boolean { @@ -99,15 +236,14 @@ export class ChartLegend extends FASTElement { return true; // Don't prevent default for Space, Enter, Tab, etc. } e.preventDefault(); - const buttons = Array.from(this.shadowRoot?.querySelectorAll('button.legend') ?? []); + // Navigate only among visible legend buttons (not the overflow trigger). + const buttons = Array.from( + this.shadowRoot?.querySelectorAll('.legend:not(.overflow-button)') ?? [], + ).filter(b => b.style.display !== 'none'); const count = buttons.length; - if (count === 0) { - return false; - } + if (count === 0) return false; const index = buttons.indexOf(e.currentTarget as HTMLButtonElement); - if (index === -1) { - return false; - } + if (index === -1) return false; const nextIndex = forward ? (index + 1) % count : (index - 1 + count) % count; buttons[index].tabIndex = -1; buttons[nextIndex].tabIndex = 0; diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts index 33884ff4a2..61a266d809 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.options.ts @@ -1,4 +1,4 @@ -export interface DonutDataPoint { +export interface DonutChartDataPoint { /** * Legend text for the datapoint in the chart */ diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts index 0d402f9e01..5c34f8e19a 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.styles.ts @@ -40,6 +40,7 @@ export const styles = css` 'chart' 'legend'; grid-template-columns: 1fr; + grid-template-rows: auto 1fr auto; width: 100%; height: 100%; position: relative; diff --git a/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts index 0d6316811a..fe980cab89 100644 --- a/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts +++ b/src/Charts.Scripts/src/donut-chart/donut-chart.template.ts @@ -12,7 +12,12 @@ export function donutChartTemplate(): ElementViewTemplate<