diff --git a/.gitignore b/.gitignore index 4e95b6f92..3cbdfa021 100644 --- a/.gitignore +++ b/.gitignore @@ -63,6 +63,7 @@ project/report/* #Docs docs/modules/ROOT/pages/ +docs/modules/ROOT/attachments/ !docs/modules/ROOT/pages/index.adoc reports/ reframe/* diff --git a/CITATION.cff b/CITATION.cff index a1dd5d448..c55a49445 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -14,7 +14,7 @@ authors: given-names: "Vincent" affiliation: "Feel++ Consortium" orcid: "https://orcid.org/0009-0005-3602-3524" -version: "3.0.2" +version: "4.0.0" doi: "10.5281/zenodo.15013241" date-released: "2025-03-12" url: "https://github.com/feelpp/benchmarking" diff --git a/docs/antora/supplemental-ui/css/figures.css b/docs/antora/supplemental-ui/css/figures.css index e0aed6204..efeaf04e7 100644 --- a/docs/antora/supplemental-ui/css/figures.css +++ b/docs/antora/supplemental-ui/css/figures.css @@ -1,31 +1,26 @@ .figure-container { position: relative; - margin: 1.5rem auto; - padding: 1rem; - background-color: #fff; - border: 1px solid #ddd; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.08); + width: 100%; + max-width: 100%; + box-sizing: border-box; + margin: 1.2rem 0; + background: none; + box-shadow: none; + padding: 0.5rem; + border: 1px solid #ccc; + border-radius: 4px; } .subfigure-container { position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; - background-color: #f9f9f9; - padding: 1rem; - border: 1px solid #ddd; - border-top: none; - border-radius: 0 0 8px 8px; + inset: 0; + background: none; + padding: 0; + border: none; } .subfigure-container.active { position: relative; - opacity: 1; - pointer-events: auto; - z-index: 1; } .subfigure-container.inactive { @@ -37,42 +32,86 @@ /* Tabs container: display buttons inline with a bottom border to indicate grouping */ .tabs-container { display: flex; - border-bottom: 2px solid #007acc; - margin-bottom: 0.5rem; + justify-content: flex-start; + gap: 0; + margin-bottom: 0.4rem; } -/* Figure tab button styling */ + .figure-tab { - background: transparent; + background: none; border: none; - outline: none; - padding: 0.5rem 1rem; - margin-right: 0.3rem; - font-size: 1rem; - color: #007acc; + font-size: 0.9rem; + color: #444; cursor: pointer; - border-radius: 4px 4px 0 0; - transition: background-color 0.2s ease, color 0.2s ease; + border: 1px solid #ccc; + padding: 0.2rem 0.5rem; } -.figure-tab:hover, -.figure-tab.active { - background-color: #007acc; - color: #fff; +.figure-tab:hover { + color: #000; /* subtle darkening on hover */ + background: none; /* no hover background */ +} + + +.export-container { + display: flex; + justify-content: flex-end; + gap: 0.3rem; + margin-bottom: 0.3rem; } .export-container button { - background-color: #007acc; - color: #fff; + background: none; + color: #666; border: none; - padding: 0.4rem 0.8rem; - margin-right: 0.4rem; - border-radius: 4px; + padding: 0; + font-size: 0.75rem; cursor: pointer; - font-size: 0.9rem; - transition: background-color 0.2s ease; + text-decoration: underline; } .export-container button:hover { - background-color: #005fa3; + color: #000; +} + +.exampleblock.plot{ + border: 1px solid #ccc; + border-radius: 4px; +} + +/* Example block styling for plots */ +.exampleblock.plot,.exampleblock.grid,.exampleblock.image { + border: none; + padding: 0; + margin: 1.5rem 0; + display: flex; + flex-direction: column; +} + +.exampleblock.plot>.content,.exampleblock.grid>.content ,.exampleblock.image>.content { + order: 1; + padding: 0; + border: none; +} + +.exampleblock.plot>.title,.exampleblock.grid>.title,.exampleblock.image>.title, .plotly-figure-caption{ + order: 2; + text-align: center; + font-style: italic; + margin-top: 0.5rem; +} + +.exampleblock.example { + border-left: 4px solid var(--brand-primary); +} + + +.download-btn.latex-btn { + position: absolute; + top: 12%; + right: 0; + margin: 16px; + border: solid 1px; + padding: 8px; } \ No newline at end of file diff --git a/docs/modules/json_report/pages/json_schema/content/figure.adoc b/docs/modules/json_report/pages/json_schema/content/figure.adoc index 4a684d344..95b22a140 100644 --- a/docs/modules/json_report/pages/json_schema/content/figure.adoc +++ b/docs/modules/json_report/pages/json_schema/content/figure.adoc @@ -10,7 +10,9 @@ The **Plot Node** defines a data visualization within the report. It specifies t |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the plot node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"plot"`.|`"plot"` +|`caption`|string|*Optional.* A descriptive caption for the figure.|`null` |`ref`|string|The unique `name` of the data file (from the root `data` list) to reference for building the figure.|*Required* |`plot`|object|The nested configuration object (`Plot` schema) defining the figure's dimensions, type, and transformation.|*Required* |=== @@ -135,7 +137,7 @@ For the given example, it produces the following dataframe [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationStrategyFactory +from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationFactory from feelpp.benchmarking.json_report.figures.schemas.plot import Plot plot_config = Plot(**{ "title": "Absolute performance", @@ -145,7 +147,7 @@ plot_config = Plot(**{ "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{"parameter":"elements", "label":"N"} }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -166,7 +168,7 @@ plot_config = Plot(**{ "secondary_axis":{ "parameter":"elements", "label":"N" } }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -189,7 +191,7 @@ plot_config = Plot(**{ "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{"parameter":"elements", "label":"N"} }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -261,16 +263,17 @@ Axis definition: [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Scatter Plot", "plot_types": [ "scatter" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("scatter",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -282,16 +285,17 @@ Axis definition: [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Marked Scatter Plot", "plot_types": [ "marked_scatter" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("marked_scatter",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -304,16 +308,17 @@ Axis definition: [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Stacked Bar Plot", "plot_types": [ "stacked_bar" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("stacked_bar",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -325,16 +330,17 @@ Axis definition: [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Grouped Bar Plot", "plot_types": [ "grouped_bar" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("grouped_bar",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -349,16 +355,17 @@ Axis definition: [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Heatmap", "plot_types": [ "heatmap" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("heatmap",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -375,16 +382,17 @@ Cell values correspond to `yaxis`. [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Table", "plot_types": [ "table" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("table",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -396,16 +404,17 @@ fig.show() [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Sunburst", "plot_types": [ "sunburst" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("sunburst",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -421,16 +430,17 @@ The `yaxis` will be shown in the line color. [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Parallel Coordinates", "plot_types": [ "parallelcoordinates" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("parallelcoordinates",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -458,16 +468,17 @@ These plots are used for visualizing three or four variables. **At least three d [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Scatter 3D", "plot_types": [ "scatter3d" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("scatter3d",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -476,16 +487,17 @@ fig.show() [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(Plot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = Plot(**{ "title": "Absolute performance - Surface 3D", "plot_types": [ "surface3d" ], "xaxis":{ "parameter":"tasks", "label":"Number of tasks" }, "yaxis":{ "parameter":"value", "label":"Execution time (s)" }, "color_axis":{ "parameter":"perfvalue", "label":"Performance variable" }, "secondary_axis":{ "parameter":"elements", "label":"N" } -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("surface3d",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- diff --git a/docs/modules/json_report/pages/json_schema/content/grid.adoc b/docs/modules/json_report/pages/json_schema/content/grid.adoc index 27ea11aa8..2cef35996 100644 --- a/docs/modules/json_report/pages/json_schema/content/grid.adoc +++ b/docs/modules/json_report/pages/json_schema/content/grid.adoc @@ -1,11 +1,13 @@ = Grid Node -The **Grid Node** is a layout node that can be used to structure content into a grid. +The **Grid Node** is a layout node that can be used to structure content into a grid. Mainly used to arrange images, plots, or tables side by side in a visually appealing manner. Sub figures or sub plots can be created using this node, but numbering and labeling must be handled manually within the captions of the child nodes. |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the grid node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"grid"`.|`"grid"` +|`caption`|string|*Optional.* A descriptive caption for the grid.|`null` |`contents`|array of objects| A list of other (non-recursive) content nodes.|[] |columns|int| Number of columns for the grid. Must be between 1 and 4.| 1 |gap|int| Must be beween 1 and 3| 2. diff --git a/docs/modules/json_report/pages/json_schema/content/image.adoc b/docs/modules/json_report/pages/json_schema/content/image.adoc index e2bb8f84d..ec20829b2 100644 --- a/docs/modules/json_report/pages/json_schema/content/image.adoc +++ b/docs/modules/json_report/pages/json_schema/content/image.adoc @@ -12,7 +12,9 @@ The `src` file path is typically resolved relative to the passed configuration f |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the image node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"image"`.|`"image"` +|`caption`|string|*Optional.* A descriptive caption for the image.|`null` |`src`|string|The file path or URL of the image to embed.|*Required* |`caption`|string|An optional caption displayed beneath the image. If provided, the image is rendered using the `title` attribute in AsciiDoc.|`null` |`alt`|string|Alternative text for the image, used for accessibility and when the image cannot be displayed.|`null` diff --git a/docs/modules/json_report/pages/json_schema/content/latex.adoc b/docs/modules/json_report/pages/json_schema/content/latex.adoc index c3d0eea92..78ae79fd1 100644 --- a/docs/modules/json_report/pages/json_schema/content/latex.adoc +++ b/docs/modules/json_report/pages/json_schema/content/latex.adoc @@ -5,6 +5,8 @@ The **LaTeX Node** is designed for the direct embedding of mathematical formulas |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the LaTeX node within the report. Used for internal cross referencing |`null` +|`is_equation`|boolean|*Optional.* Indicates if the LaTeX content represents a standalone equation. It adds `\begin\{equation\}` and `\end\{equation\}` around the content if true.|`false` |`type`|string|Must be set to `"latex"`.|`"latex"` |`latex`|string|The raw LaTeX content (e.g., a mathematical equation or a custom LaTeX block).|*Required* |`ref`|string|The `name` of the data field to reference for dynamic placeholder resolution.|`null` diff --git a/docs/modules/json_report/pages/json_schema/content/list.adoc b/docs/modules/json_report/pages/json_schema/content/list.adoc index e3067e104..de605e914 100644 --- a/docs/modules/json_report/pages/json_schema/content/list.adoc +++ b/docs/modules/json_report/pages/json_schema/content/list.adoc @@ -7,6 +7,7 @@ The **List Node** is used to render ordered lists. Its primary strength is that |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the list node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"itemize"`.|`"itemize"` |`items`|array of objects/strings|The contents of the list. Each entry can be a simple string, a full `Text Node` object, or a `Text Configuration` object.|*Required* |`ref`|string|The `name` of the data field to reference for dynamic placeholder resolution within all list items.|`null` diff --git a/docs/modules/json_report/pages/json_schema/content/section.adoc b/docs/modules/json_report/pages/json_schema/content/section.adoc index 609ade1e9..549c22205 100644 --- a/docs/modules/json_report/pages/json_schema/content/section.adoc +++ b/docs/modules/json_report/pages/json_schema/content/section.adoc @@ -5,6 +5,7 @@ The **Section Node** is the fundamental building block for structuring your repo |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the section node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"section"`.|`"section"` |`title`|string|The title of the section.|*Required* |`contents`|array of objects|A recursive list of other content nodes, including nested `SectionNode`s.| [] diff --git a/docs/modules/json_report/pages/json_schema/content/table.adoc b/docs/modules/json_report/pages/json_schema/content/table.adoc index 5ffd6d3a6..3b82b09a0 100644 --- a/docs/modules/json_report/pages/json_schema/content/table.adoc +++ b/docs/modules/json_report/pages/json_schema/content/table.adoc @@ -10,6 +10,8 @@ The structure is composed of the `TableNode` container and the nested `Table` co |=== |Field|Type|Description|Default Value + +|`id`|string| *Optional.* Identifies the table node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"table"`.|`"table"` |`ref`|string|The unique reference of the data file (from the root `data` list) containing the table to be rendered.|*Required* |`caption`|string|*Optional.* Table caption.| `null` diff --git a/docs/modules/json_report/pages/json_schema/content/text.adoc b/docs/modules/json_report/pages/json_schema/content/text.adoc index 34d543fe4..5cc3de0a9 100644 --- a/docs/modules/json_report/pages/json_schema/content/text.adoc +++ b/docs/modules/json_report/pages/json_schema/content/text.adoc @@ -5,6 +5,7 @@ The **Text Node** is used to insert narrative content into your report. It suppo |=== |Field|Type|Description|Default Value +|`id`|string| *Optional.* Identifies the text node within the report. Used for internal cross referencing |`null` |`type`|string|Must be set to `"text"`.|`"text"` |`text`|string or object|The text content or a nested `Text` object.|*Required* |`ref`|string|The `name` of the data field to reference for dynamic placeholder resolution.|`null` diff --git a/docs/modules/json_report/pages/json_schema/referencing.adoc b/docs/modules/json_report/pages/json_schema/referencing.adoc new file mode 100644 index 000000000..be8fbd378 --- /dev/null +++ b/docs/modules/json_report/pages/json_schema/referencing.adoc @@ -0,0 +1,81 @@ += Internal Cross References + +It is possible to assign an optional unique identifier to any content node within the JSON report schema. This identifier can then be used to create internal cross-references within the report, allowing for easy navigation between different sections or elements. + +== Assigning Identifiers + +To assign an identifier to a content node, include the `id` field in the node's JSON object. The value of this field should be a unique string that serves as the identifier for that node. + +[source,json] +---- +{ + "type": "section", + "id": "introduction", + "title": "Introduction", + "contents": [ + // Other content nodes + ] +} +---- +In this example, the section node is assigned the identifier `"introduction"`. + +[IMPORTANT] +Ensure that each id contains only alphanumeric characters, underscores, or hyphens, and does not start with a number. This ensures compatibility with AsciiDoc cross-referencing conventions. + +== Figure and Table Numbering + +Tables, figures and images are automatically numbered in the order they appear in the report. However, tables will be prefixed with "Table", figures, plots and images will be prefixed with "Figure". This means that automatic numbering will be independent between tables and figures/images. + +[IMPORTANT] +Elements included inside a grid node are NOT automatically numbered. You must manually include numbering in the caption if desired (e.g. "Figure 1a: ..."). The counter for the automatic numbering will not be incremented for these elements. + +== Creating Cross-References + +To create a cross-reference to a content node with an assigned identifier, use the link:https://docs.asciidoctor.org/asciidoc/latest/macros/xref/#internal-cross-references[AsciiDoc cross-referencing syntax]. The format is `\<>`, where `id` is the identifier of the target node, and `Display Text` is the text that will be displayed as the link in the report. + +[source,asciidoc] +---- +See <> for more details. +---- + +In this example, the text "Introduction Section" will be a clickable link that navigates to the section with the identifier `"introduction"`. + + +[IMPORTANT] +Ensure that the identifiers used in cross-references match exactly with those assigned to the content nodes, including case sensitivity. Mismatched identifiers will result in broken links in the final report. + +== Example + +[source,json] +---- +{ + "contents": [ + { + "type":"text", + "id":"intro_text", + "text":"Welcome to the report." + }, + { + "type":"section", + "id":"data_analysis", + "title":"Data Analysis", + "contents":[ + { + "type":"table", + "id":"results_table", + "ref":"results_data", + "caption":"Results Overview" + }, + { + "type":"text", + "text":"Refer to <> for context." + } + ] + }, + { + "type":"text", + "text":"See <> for detailed metrics." + } + ] +} +---- \ No newline at end of file diff --git a/docs/modules/json_report/pages/json_schema/report_schema.adoc b/docs/modules/json_report/pages/json_schema/report_schema.adoc index 2712975c6..f85d9efa3 100644 --- a/docs/modules/json_report/pages/json_schema/report_schema.adoc +++ b/docs/modules/json_report/pages/json_schema/report_schema.adoc @@ -58,7 +58,7 @@ include::data.adoc[leveloffset=+1] include::content/section.adoc[leveloffset=+1] -include::content/section.adoc[leveloffset=+1] +include::content/grid.adoc[leveloffset=+1] include::content/text.adoc[leveloffset=+1] @@ -70,4 +70,6 @@ include::content/image.adoc[leveloffset=+1] include::content/table.adoc[leveloffset=+1] -include::content/figure.adoc[leveloffset=+1] \ No newline at end of file +include::content/figure.adoc[leveloffset=+1] + +include::referencing.adoc[leveloffset=+1] \ No newline at end of file diff --git a/docs/modules/tutorial/pages/configurationfiles/plots.adoc b/docs/modules/tutorial/pages/configurationfiles/plots.adoc index a2f94ed81..dcb1163bc 100644 --- a/docs/modules/tutorial/pages/configurationfiles/plots.adoc +++ b/docs/modules/tutorial/pages/configurationfiles/plots.adoc @@ -116,7 +116,7 @@ For the given example, it produces the following dataframe [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationStrategyFactory +from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationFactory from feelpp.benchmarking.reframe.schemas.benchmarkSchemas import DefaultPlot plot_config = DefaultPlot(**{ "title": "Absolute performance", @@ -125,7 +125,7 @@ plot_config = DefaultPlot(**{ "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"} }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -146,7 +146,7 @@ plot_config = DefaultPlot(**{ "secondary_axis":{ "parameter":"elements", "label":"N" } }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -168,7 +168,7 @@ plot_config = DefaultPlot(**{ "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"} }) -strategy = TransformationStrategyFactory.create(plot_config) +strategy = TransformationFactory.create(plot_config) df = strategy.calculate(master_df) print(df) ---- @@ -181,15 +181,16 @@ Considering the same example axis as above, the software can generate the follow [%dynamic%open%hide_code,python] ---- -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -figures = FigureFactory.create(DefaultPlot(**{ +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory +plot_config = DefaultPlot(**{ "title": "Absolute performance - Scatter Plot", "plot_types": [ "scatter" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("scatter",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -204,14 +205,15 @@ This plot type will behave as follows: [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Marked Scatter Plot", "plot_types": [ "marked_scatter" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("marked_scatter",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -219,14 +221,15 @@ fig.show() [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Stacked Bar Plot", "plot_types": [ "stacked_bar" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("stacked_bar",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -235,14 +238,15 @@ fig.show() [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Grouped Bar Plot", "plot_types": [ "grouped_bar" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("grouped_bar",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -252,15 +256,16 @@ For this case, we will consider the `elements` (N) as `color_axis` and `performa [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - HeatMap", "plot_types": [ "heatmap" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"perfvalue", "label":"Performance Variable"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"}, "color_axis":{"parameter":"elements", "label":"N"}, -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("heatmap",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -268,14 +273,15 @@ fig.show() [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Table", "plot_types": [ "table" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("table",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -288,14 +294,15 @@ The `secondary_axis` and `xaxis` parameter are present respectively on the inner [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Sunburst Plot", "plot_types": [ "sunburst" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("sunburst",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -306,14 +313,15 @@ Axes will be shown on the following order: `secondary_axis`, `xaxis`, all additi [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ +plot_config = DefaultPlot(**{ "title": "Absolute performance - Parallel Coordinates Plot", "plot_types": [ "parallelcoordinates" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("parallelcoordinates",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -333,14 +341,15 @@ Axes correspondance is as follows: [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ - "title": "Absolute performance - Scatter 3D", +plot_config = DefaultPlot(**{ + "title": "absolute performance - Scatter 3D", "plot_types": [ "scatter3d" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("scatter3d",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- @@ -348,14 +357,15 @@ fig.show() [%dynamic%open%hide_code,python] ---- -figures = FigureFactory.create(DefaultPlot(**{ - "title": "Absolute performance - Surface 3D", +plot_config = DefaultPlot(**{ + "title": "absolute performance - Surface 3D", "plot_types": [ "surface3d" ], "yaxis":{"label":"Execution time (s)"}, "secondary_axis":{"parameter":"elements", "label":"N"}, "xaxis":{"parameter":"tasks", "label":"Number of tasks"} -})) -fig = figures[0].createFigure(master_df) +}) +figures = FigureFactory.create("surface3d",plot_config) +fig = figures.createFigure(TransformationFactory.create(plot_config).calculate(master_df)) fig.show() ---- diff --git a/examples/matrixvector/plots.json b/examples/matrixvector/plots.json index 1f3188743..6d98c7ea4 100644 --- a/examples/matrixvector/plots.json +++ b/examples/matrixvector/plots.json @@ -46,7 +46,7 @@ "contents":[ { "type":"table", - "ref": "reframe_df", + "ref": "parameter_table", "layout":{ "rename":{ "testcases.time_total":"Total Time (s)", diff --git a/examples/parallelsum/plots.json b/examples/parallelsum/plots.json index e239a4058..016514012 100644 --- a/examples/parallelsum/plots.json +++ b/examples/parallelsum/plots.json @@ -47,33 +47,22 @@ "contents": [ { "type": "table", - "ref": "reframe_df", + "ref": "parameter_table", "layout": { - "columns": ["result", "testcases.hashcode", "tasks", "elements", "testcases.time_total"], - "computed_columns":{ - "logs_link":"f'link:logs/{row[\"testcases.hashcode\"]}.html[Logs]'" - }, "rename": { "testcases.time_total": "Total Time (s)", "testcases.hashcode": "Hash", "result": "", "logs_link":"" }, - "group_by": { "columns": ["testcases.hashcode"], "agg": "first" }, - "format": { - "testcases.time_total": "%.3f", - "tasks":"%.0f", - "elements":"%.0f", - "result": { "pass": "🟢", "fail": "🔴" } - }, - "column_order": ["result", "testcases.hashcode", "tasks", "elements", "testcases.time_total","logs_link"], - "style": { - "column_align": { "result": "center" }, - "column_width":{ "result":1,"logs_link":1}, - "classnames": ["scrollable", "sortable"] - } + "column_order": ["result", "testcases.hashcode", "tasks", "elements", "testcases.time_total","logs_link"] }, - "filter": { "placeholder": "Filter testcases..." } + "filter": { "placeholder": "Filter testcases..." }, + "style": { + "column_align": { "result": "center" }, + "column_width":{ "result":1,"logs_link":1}, + "classnames": ["scrollable", "sortable"] + } } ] }, diff --git a/examples/sorting/plots.json b/examples/sorting/plots.json index 9e9deb3ed..4fa4cbe99 100644 --- a/examples/sorting/plots.json +++ b/examples/sorting/plots.json @@ -47,7 +47,7 @@ "contents": [ { "type": "table", - "ref": "reframe_df", + "ref": "parameter_table", "layout": { "rename": { "testcases.time_total": "Execution Time (s)", diff --git a/pyproject.toml b/pyproject.toml index fc727efdc..7ff1f5de3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "setuptools.build_meta" where = ["src"] [tool.setuptools.package-data] -'feelpp.benchmarking' = ['json_report/templates/**','dashboardRenderer/templates/**','json_report/figures/templates/**','report/templates/**','scripts/data/*','scripts/data/website_images/*', 'reframe/templates/**'] +'feelpp.benchmarking' = ['json_report/templates/**','dashboardRenderer/templates/**','json_report/figures/tikz/templates/**','report/templates/**','scripts/data/*','scripts/data/website_images/*', 'reframe/templates/**'] [project] name = "feelpp-benchmarking" @@ -43,7 +43,8 @@ dependencies = [ "Jinja2", "numpy", "pandas", - "IPython" + "IPython", + "pyyaml" ] [project.urls] diff --git a/src/feelpp/benchmarking/dashboardRenderer/component/base.py b/src/feelpp/benchmarking/dashboardRenderer/component/base.py index 9f6fbf8a2..86ae12281 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/component/base.py +++ b/src/feelpp/benchmarking/dashboardRenderer/component/base.py @@ -179,7 +179,7 @@ def getPathToRoot( self ) -> List[List["GraphNode"]]: paths.append([self] + parent_path) return paths - def render( self, base_dir:str, parent_id:str = None, renderLeaves = True ) -> None: + def render( self, base_dir:str, parent_id:str = None, renderLeaves = True, **kwargs) -> None: """ Renders the node's view and recursively calls render on its children. @@ -205,13 +205,13 @@ def render( self, base_dir:str, parent_id:str = None, renderLeaves = True ) -> N ) ) - self.view.renderExtra( component_dir ) - self.view.render( component_dir ) + self.view.renderExtra( component_dir, **kwargs ) + self.view.render( component_dir, **kwargs ) for child in self.children: if not renderLeaves and child.isLeaf(): continue - child.render( component_dir, new_parent_id, renderLeaves ) + child.render( component_dir, new_parent_id, renderLeaves, **kwargs ) def upstreamViewData( self, aggregator: Callable[[str, Optional[str], dict, List], dict] ) -> dict: diff --git a/src/feelpp/benchmarking/dashboardRenderer/component/leaf.py b/src/feelpp/benchmarking/dashboardRenderer/component/leaf.py index ae2b6f9c4..18b2f9f47 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/component/leaf.py +++ b/src/feelpp/benchmarking/dashboardRenderer/component/leaf.py @@ -50,7 +50,7 @@ def getPermParentIdsStr( self ) -> str: branches.append( "-".join([b.id for b in branch[1:][::-1]]) ) return ",".join( branches ) - def render( self, base_dir:str ) -> None: + def render( self, base_dir:str, **kwargs ) -> None: """ Renders the leaf component's view in its dedicated subdirectory. @@ -71,9 +71,9 @@ def render( self, base_dir:str ) -> None: ) ) self.view.copyPartials( leaf_dir, os.path.join(base_dir,"..") ) - self.view.renderExtra( leaf_dir ) + self.view.renderExtra( leaf_dir, **kwargs ) - self.view.render( leaf_dir ) + self.view.render( leaf_dir, **kwargs ) def patchTemplateInfo( self, patch : Union[dict,TemplateDataFile], prefix:str, save:bool = False ) -> None: """ diff --git a/src/feelpp/benchmarking/dashboardRenderer/core/dashboard.py b/src/feelpp/benchmarking/dashboardRenderer/core/dashboard.py index 0a0eb7a89..e84621e88 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/core/dashboard.py +++ b/src/feelpp/benchmarking/dashboardRenderer/core/dashboard.py @@ -51,7 +51,7 @@ def print( self ) -> None: """ Prints the hierarchical structure of the component tree for debugging or review.""" self.tree.print() - def render( self, base_path:str, clean:bool = False ): + def render( self, base_path:str, clean:bool = False, project_name="", **kwargs ): """ Triggers the rendering process, generating the final dashboard files. Args: @@ -59,11 +59,16 @@ def render( self, base_path:str, clean:bool = False ): clean (bool, optional): If True, deletes the existing 'pages' directory before rendering. Defaults to False. """ pages_dir = os.path.join( base_path, "pages" ) + attachments_dir = os.path.join( base_path, "attachments" ) + attachments_base_url = os.path.join( f"/{project_name}/_attachments" ) - if clean and os.path.isdir(pages_dir): - shutil.rmtree(pages_dir) + if clean: + if os.path.isdir(pages_dir): + shutil.rmtree(pages_dir) + if os.path.isdir(attachments_dir): + shutil.rmtree(attachments_dir) - self.tree.render(pages_dir) + self.tree.render(pages_dir, attachments_dirpath = attachments_dir, attachments_base_url = attachments_base_url, **kwargs) def patchTemplateInfo( self, patches:List[str], targets:str, prefix:str, save:bool = False ): """ diff --git a/src/feelpp/benchmarking/dashboardRenderer/core/treeBuilder.py b/src/feelpp/benchmarking/dashboardRenderer/core/treeBuilder.py index f927a0893..5622cc499 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/core/treeBuilder.py +++ b/src/feelpp/benchmarking/dashboardRenderer/core/treeBuilder.py @@ -123,7 +123,7 @@ def _buildSubtree( parent_node: TreeNode, subtree: dict, depth:int = 0 ): c.parents.remove(b[-1]) b[-1].children.clear() - def render( self, base_path:str ) -> None: + def render( self, base_path:str, **kwargs) -> None: """ Renders the entire component tree structure to disk. It renders all repository and node pages (via the base class render) and then delegates the rendering of all leaf components to the LeafComponentRepository. @@ -131,8 +131,8 @@ def render( self, base_path:str ) -> None: Args: base_path (str): The root directory where the dashboard will be generated. """ - super().render(base_path,None,renderLeaves=False) - self.leaf_repository.render(base_path) + super().render(base_path,None,renderLeaves=False, **kwargs) + self.leaf_repository.render(base_path, **kwargs) def patchTemplateInfo( self, patches:list[str], targets:str, prefix:str, save:bool = False ): """ diff --git a/src/feelpp/benchmarking/dashboardRenderer/renderer.py b/src/feelpp/benchmarking/dashboardRenderer/renderer.py index a1506454e..0dcac8019 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/renderer.py +++ b/src/feelpp/benchmarking/dashboardRenderer/renderer.py @@ -29,7 +29,7 @@ def __init__( self, template_paths: Union[List[str],str], template_filename:str self.template = self.env.get_template( template_filename ) - def render( self, output_filepath:str, data:Dict[str,Any]={} ) -> None: + def render( self, output_filepath:str, data:Dict[str,Any]={}, **kwargs ) -> None: """ Render the provided data into the primary template and write the output to the specified file path. @@ -42,7 +42,7 @@ def render( self, output_filepath:str, data:Dict[str,Any]={} ) -> None: data.update({"self_dirpath":os.path.dirname(output_filepath)}) with open( output_filepath, 'w' ) as f: - f.write( self.template.render(data) ) + f.write( self.template.render(data,**kwargs) ) def setGlobals( self ) -> None: """Configures global variables available to all templates loaded by this environment.""" @@ -71,7 +71,7 @@ def stripQuotes( value:Any ) -> Any: return value.strip('"') return value - def renderTemplate( self, template_path:str, data:Dict[str,Any], destination:str ) -> None: + def renderTemplate( self, template_path:str, data:Dict[str,Any], destination:str, **kwargs ) -> None: """ Renders an arbitrary, secondary template file within the environment to a specific destination. @@ -87,7 +87,7 @@ def renderTemplate( self, template_path:str, data:Dict[str,Any], destination:str if not os.path.isdir( dest_dir ): os.makedirs( dest_dir ) with open( destination, 'w' ) as f: - f.write( template.render( data ) ) + f.write( template.render( data, **kwargs ) ) class BaseRendererFactory: """ diff --git a/src/feelpp/benchmarking/dashboardRenderer/repository/leaf.py b/src/feelpp/benchmarking/dashboardRenderer/repository/leaf.py index b585a2b42..d1d40a6c9 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/repository/leaf.py +++ b/src/feelpp/benchmarking/dashboardRenderer/repository/leaf.py @@ -50,7 +50,7 @@ def collectMetadata( mapping:Dict[str,Any], path:Optional[List[str]] = [] ) -> L collected += LeafComponentRepository.collectMetadata( v, path + [k] ) return collected - def render( self, base_dir:str ) -> None: + def render( self, base_dir:str, **kwargs ) -> None: """ Renders all Leaf Components stored in the repository. @@ -63,5 +63,5 @@ def render( self, base_dir:str ) -> None: if not os.path.isdir( leaves_dir ): os.mkdir( leaves_dir ) for leaf in self.data: - leaf.render( leaves_dir ) + leaf.render( leaves_dir, **kwargs ) diff --git a/src/feelpp/benchmarking/dashboardRenderer/views/base.py b/src/feelpp/benchmarking/dashboardRenderer/views/base.py index b3425cda0..5f6f9d500 100644 --- a/src/feelpp/benchmarking/dashboardRenderer/views/base.py +++ b/src/feelpp/benchmarking/dashboardRenderer/views/base.py @@ -130,7 +130,7 @@ def copyPartials( self, base_dir:str, pages_dir:str ) -> None: shutil.copytree(path, local_partial_path,dirs_exist_ok=True) self.updateTemplateData({prefix:os.path.relpath(local_partial_path,pages_dir)}) - def renderExtra( self, base_dir:str ) -> None: + def renderExtra( self, base_dir:str, **kwargs) -> None: """ Renders all extra renderers associated with this view. Each extra renderer is expected to have its own rendering logic and output path. @@ -140,12 +140,12 @@ def renderExtra( self, base_dir:str ) -> None: """ extra_renders = [] for prefix,renderer in self.extra_renderers.items(): - output_filepath = renderer.render( base_dir ) + output_filepath = renderer.render( base_dir, **kwargs ) self.updateTemplateData({prefix:os.path.relpath(output_filepath,base_dir)}) extra_renders.append(os.path.relpath(output_filepath,base_dir)) self.updateTemplateData({"extra_renders":extra_renders}) - def render( self, output_dirpath:str, filename:Optional[str] = None ) -> None: + def render( self, output_dirpath:str, filename:Optional[str] = None, **kwargs ) -> None: """ Executes the final rendering step, writing the output to a file. Args: @@ -161,7 +161,7 @@ def render( self, output_dirpath:str, filename:Optional[str] = None ) -> None: if not os.path.isdir(output_dirpath): os.mkdir(output_dirpath) - self.renderer.render(os.path.join(output_dirpath,filename),self.template_data) + self.renderer.render(os.path.join(output_dirpath,filename),self.template_data,**kwargs) def processPlugins( self ) -> None: """ diff --git a/src/feelpp/benchmarking/json_report/__main__.py b/src/feelpp/benchmarking/json_report/__main__.py index 48bf813fa..e3ef27f4a 100644 --- a/src/feelpp/benchmarking/json_report/__main__.py +++ b/src/feelpp/benchmarking/json_report/__main__.py @@ -8,7 +8,7 @@ def main_cli(): parser = ArgumentParser( description="Generates structured documents (like AsciiDoc) from a declarative JSON configuration file.", prog='json-report-render' ) parser.add_argument( 'REPORT_FILE', type=str, help='The path to the declarative JSON report configuration file.' ) parser.add_argument( '-o', '--output-dir', type=str, default='.', help='The directory where the final generated report will be saved.') - parser.add_argument( '-f', '--output-format', type=str, default='adoc', choices=['adoc'], help='The format of the final document to be generated. Currently supports: adoc.' ) + parser.add_argument( '-f', '--output-format', type=str, default='adoc', choices=['adoc','tex'], help='The format of the final document to be generated. Currently supports: adoc.' ) parser.add_argument( '-n', '--output-name', type=str,default=None, help='A specific name for the output file (e.g., final_report.adoc). If not provided, the name is inferred from the REPORT_FILE.' ) args = parser.parse_args() diff --git a/src/feelpp/benchmarking/json_report/figures/base.py b/src/feelpp/benchmarking/json_report/figures/base.py index c41a09b71..f177c4db2 100644 --- a/src/feelpp/benchmarking/json_report/figures/base.py +++ b/src/feelpp/benchmarking/json_report/figures/base.py @@ -1,9 +1,9 @@ +import os from pandas import MultiIndex class Figure: - def __init__(self,plot_config,transformation_strategy): + def __init__(self,plot_config): self.config = plot_config - self.transformation_strategy = transformation_strategy def getIdealRange(self,df): """ Computes the [(min - eps), (max+eps)] interval for optimal y-axis display @@ -14,55 +14,43 @@ def getIdealRange(self,df): range_epsilon= 0.01 return [ df.min().min() - df.min().min()*range_epsilon, df.max().max() + df.min().min()*range_epsilon ] - def createMultiindexFigure(self,df): + def createMultiindexFigure(self,df,data_dirpath): raise NotImplementedError("Pure virtual function. Not to be called from the base class") - def createSimpleFigure(self,df): + def createSimpleFigure(self,df,data_dirpath): raise NotImplementedError("Pure virtual function. Not to be called from the base class") + def sanitizeFilename(self,title:str): + """Creates a FS friendly filename. Mostly used to be latex compatible""" + t = str(title).replace(" ","-") + return "".join(x for x in t if x.isalnum() or x in ["_","-","."]) + def createCsvs(self,df): """Creates the corresponding csv strings for the figure Args: df (pd.DataFrame). The master dataframe containing all reframe test data Returns: list[dict[str,str]]: A list of dictionaries containing the csv strings and their corresponding titles. - Schema: [{"title":str, "data":str}] + Schema: [{"filename":str, "data":str}] """ - df = self.transformation_strategy.calculate(df) if isinstance(df.index,MultiIndex): - return [{"title":key, "data":df.xs(key, level=0).to_csv()} for key in df.index.levels[0]] + return [{"filename":f"{self.sanitizeFilename(key)}.csv", "data":df.xs(key, level=0).to_csv()} for key in df.index.levels[0]] else: - return [{"title":self.config.title, "data":df.to_csv()}] + return [{"filename":f"{self.sanitizeFilename(self.config.title)}.csv", "data":df.to_csv()}] - def createFigure(self,df, **args): + def createFigure(self,df,data_dirpath, **args): """ Creates a figure from the master dataframe Args: df (pd.DataFrame). The master dataframe containing all reframe test data Returns: go.Figure: Plotly figure corresponding to the grouped Bar type """ - df = self.transformation_strategy.calculate(df) if isinstance(df.index,MultiIndex): - figure = self.createMultiindexFigure(df, **args) + figure = self.createMultiindexFigure(df,data_dirpath, **args) else: - figure = self.createSimpleFigure(df, **args) + figure = self.createSimpleFigure(df, data_dirpath, **args) return figure - -class CompositeFigure: - def createFigure(self, df): - return self.plotly_figure.createFigure(df) - - def createTex(self, df): - if self.tikz_figure is not None: - return self.tikz_figure.createFigure(df) - else: - print(f"Warning: Tikz figure not implemented for plot type {self.__class__.__name__}") - return None - - def createCsvs(self,df): - return self.plotly_figure.createCsvs(df) - - def createFigureHtml(self,df): - return self.plotly_figure.createHtml(df) \ No newline at end of file + def createJson(self,df): + raise NotImplementedError diff --git a/src/feelpp/benchmarking/json_report/figures/controller.py b/src/feelpp/benchmarking/json_report/figures/controller.py index 0ad4fad02..952924a18 100644 --- a/src/feelpp/benchmarking/json_report/figures/controller.py +++ b/src/feelpp/benchmarking/json_report/figures/controller.py @@ -1,54 +1,88 @@ -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory -from feelpp.benchmarking.json_report.figures.schemas.plot import Plot +import os, zipfile from typing import List, Dict, Union +import pandas as pd +from uuid import uuid4 + +from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationFactory +from feelpp.benchmarking.json_report.figures.plotly.figureFactory import FigureFactory as PlotlyFigureFactory +from feelpp.benchmarking.json_report.figures.tikz.figureFactory import FigureFactory as TikzFigureFactory +from feelpp.benchmarking.json_report.figures.schemas.plot import Plot class Controller: - """ Controller component , it orchestrates the model with the view""" - def __init__(self, df, plots_config: Union[List[Dict],Dict,Plot,List[Plot]]): - """ - Args: - model (pd.DataFrame): The atomic report model component - view (AtomicReportView): The atomic report view component - """ - self.df = df - if isinstance(plots_config,Plot): - self.plots_config = [plots_config] - elif isinstance(plots_config,Dict): - self.plots_config = [Plot(**plots_config)] - elif isinstance(plots_config,List): - if all(isinstance(d,Plot) for d in plots_config): - self.plots_config = plots_config - else: - self.plots_config = [Plot(**d) for d in plots_config] - else: - raise TypeError(f"plots_config must be a Dict or List of Dicts, got {type(plots_config)}") - self.figures = [FigureFactory.create(plot_config) for plot_config in self.plots_config] - - def generateAll(self): - if self.df is None or self.df.empty: - return [] - return [ - self.generateFigure(figure,plot_config.plot_types) - for figure,plot_config in zip(self.figures,self.plots_config) - ] - - def generateFigure(self,figure,plot_types): - return { - "plot_types": plot_types, - "subfigures": [self.generateSubfigure(subfigure) for subfigure in figure] + """ + One semantic figure: + - same data + - multiple transformations + - multiple views + - multiple exports + """ + def __init__(self, data:pd.DataFrame, plot_config: Union[Dict,Plot], report_uuid:str = None): + self.report_uuid = report_uuid + self.id = uuid4().hex + if not isinstance(data,pd.DataFrame): + raise NotImplementedError(f"Data type {type(data)} not supported for Figures") + self.plot_config = self.coercePlotConfig(plot_config) + + #TODO: allow multiple transformations in the future + self.transformed = { self.plot_config.transformation : TransformationFactory.create(self.plot_config).calculate(data) } + + self.figure_views = { + plot_type : { + "plotly": lambda pt=plot_type : PlotlyFigureFactory.create(pt,plot_config=self.plot_config), + "latex": lambda pt=plot_type : TikzFigureFactory.create(pt,plot_config=self.plot_config) + } + for plot_type in self.plot_config.plot_types } - def generateSubfigure(self, subfigure): - return { - "exports": [ - { "display_text":"CSV", "data":[ - { "format":"csv", "prefix":"data","content":subfigure.createCsvs(self.df)} - ]}, - { "display_text":"LaTeX", "data":[ - {"format":"tex","content":[{ "data":subfigure.createTex(self.df), "title":"figures" }]}, - {"format":"csv","content":subfigure.createCsvs(self.df)} - - ]}, - ], - "html": subfigure.createFigureHtml(self.df) - } \ No newline at end of file + def coercePlotConfig(self, config): + if isinstance(config,Plot): + plot_config = config + elif isinstance(config,Dict): + plot_config = Plot(**config) + else: + raise TypeError(f"plot_config must be a Dict or a Plot model, got {type(config)}") + return plot_config + + + def renderFigure(self, plot_type, backend, transformation, data_dir = "." ): + figure = self.figure_views[plot_type][backend]() + if not figure: + return None + return figure.createFigure(self.transformed[transformation],data_dir) + + def exportFigureData(self, plot_type, backend, transformation, formats=["csv"], outdir:str = ".") -> list[dict[str,str]]: + figure = self.figure_views[plot_type][backend]() + if not figure: + return None + if self.report_uuid: + relpath = os.path.join(self.report_uuid,self.id,f"{plot_type}") + else: + relpath = os.path.join(self.id,f"{plot_type}") + + filepath = os.path.join(outdir,relpath) + + os.makedirs(os.path.dirname(filepath),exist_ok=True) + + exported_paths = {} + if "json" in formats: + with open(f"{filepath}.json","w") as f: + f.write(figure.createJson(self.transformed[transformation])) + + exported_paths["json"] = f"{relpath}.json" + if "csv" in formats: + os.mkdir(filepath) + csvs = figure.createCsvs(self.transformed[transformation]) + for csv in csvs: + csv_fn = os.path.join(filepath,f"{csv['filename']}") + with open(csv_fn,"w") as f: + f.write(csv['data']) + exported_paths["csv"] = relpath + + if "zip_csv" in formats: + csvs = figure.createCsvs( self.transformed[transformation] ) + with zipfile.ZipFile( file=f"{filepath}.zip", mode="w", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as zip_archive: + for csv in csvs: + zip_archive.writestr(zinfo_or_arcname=f"{csv['filename']}",data=csv['data']) + exported_paths["zip_csv"] = f"{relpath}.zip" + + return exported_paths \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/figureFactory.py b/src/feelpp/benchmarking/json_report/figures/figureFactory.py deleted file mode 100644 index dbc66b67c..000000000 --- a/src/feelpp/benchmarking/json_report/figures/figureFactory.py +++ /dev/null @@ -1,107 +0,0 @@ -from feelpp.benchmarking.json_report.figures.transformationFactory import TransformationStrategyFactory - -from feelpp.benchmarking.json_report.figures.base import CompositeFigure -from feelpp.benchmarking.json_report.figures.tikzFigures import TikzFigure, TikzScatterFigure, TikzGroupedBarFigure, TikzStackedBarFigure, TikzTableFigure -from feelpp.benchmarking.json_report.figures.plotlyFigures import PlotlyFigure, PlotlyScatterFigure, PlotlyGroupedBarFigure, PlotlyStackedBarFigure, PlotlyTableFigure, PlotlyHeatmapFigure, PlotlySunburstFigure, PlotlyScatter3DFigure, PlotlySurface3DFigure, PlotlyParallelcoordinatesFigure, PlotlyMarkedScatter - - - -class ScatterFigure(CompositeFigure): - """ Composite figure class for scatter figures""" - def __init__(self, plot_config, transformation_strategy, fill_lines=[]): - self.plotly_figure = PlotlyScatterFigure(plot_config,transformation_strategy,fill_lines) - self.tikz_figure = TikzScatterFigure(plot_config,transformation_strategy,fill_lines) - -class TableFigure(CompositeFigure): - """ Composite figure class for table figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyTableFigure(plot_config,transformation_strategy) - self.tikz_figure = TikzTableFigure(plot_config,transformation_strategy) - -class StackedBarFigure(CompositeFigure): - """ Composite figure class for stacked bar figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyStackedBarFigure(plot_config,transformation_strategy) - self.tikz_figure = TikzStackedBarFigure(plot_config,transformation_strategy) - -class GroupedBarFigure(CompositeFigure): - """ Composite figure class for grouped bar figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyGroupedBarFigure(plot_config,transformation_strategy) - self.tikz_figure = TikzGroupedBarFigure(plot_config,transformation_strategy) - -class HeatmapFigure(CompositeFigure): - """ Composite figure class for heatmap figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyHeatmapFigure(plot_config,transformation_strategy) - self.tikz_figure = None - -class SunburstFigure(CompositeFigure): - """ Composite figure class for sunburst figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlySunburstFigure(plot_config,transformation_strategy) - self.tikz_figure = None - -class Scatter3DFigure(CompositeFigure): - """ Composite figure class for 3D scatter figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyScatter3DFigure(plot_config,transformation_strategy) - self.tikz_figure = None - -class Surface3DFigure(CompositeFigure): - """ Composite figure class for 3D surface figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlySurface3DFigure(plot_config,transformation_strategy) - self.tikz_figure = None - -class ParallelcoordinatesFigure(CompositeFigure): - """ Composite figure class for parallel coordinates figures""" - def __init__(self, plot_config, transformation_strategy): - self.plotly_figure = PlotlyParallelcoordinatesFigure(plot_config,transformation_strategy) - self.tikz_figure = None - -class MarkedScatterFigure(CompositeFigure): - def __init__(self, plot_config, transformation_strategy,fill_lines): - self.plotly_figure = PlotlyMarkedScatter(plot_config,transformation_strategy,fill_lines) - self.tikz_figure = None - - -class FigureFactory: - """ Factory class to dispatch concrete figure elements""" - @staticmethod - def create(plot_config): - """ Creates a concrete composite figure element - Args: - plot_config (Plot). Pydantic object with the plot configuration information - """ - strategy = TransformationStrategyFactory.create(plot_config) - figures = [] - for plot_type in plot_config.plot_types: - if plot_type in ["scatter","marked_scatter"]: - fill_lines = [] - if plot_config.transformation=="speedup": - fill_lines = ["optimal","half-optimal"] - if plot_type == "scatter": - figures.append(ScatterFigure(plot_config,strategy, fill_lines)) - elif plot_type == "marked_scatter": - figures.append(MarkedScatterFigure(plot_config,strategy, fill_lines)) - elif plot_type == "table": - figures.append(TableFigure(plot_config,strategy)) - elif plot_type == "stacked_bar": - figures.append(StackedBarFigure(plot_config,strategy)) - elif plot_type == "grouped_bar": - figures.append(GroupedBarFigure(plot_config,strategy)) - elif plot_type == "heatmap": - figures.append(HeatmapFigure(plot_config,strategy)) - elif plot_type == "sunburst": - figures.append(SunburstFigure(plot_config,strategy)) - elif plot_type == "scatter3d": - figures.append(Scatter3DFigure(plot_config,strategy)) - elif plot_type == "surface3d": - figures.append(Surface3DFigure(plot_config,strategy)) - elif plot_type == "parallelcoordinates": - figures.append(ParallelcoordinatesFigure(plot_config,strategy)) - else: - raise NotImplementedError - - return figures \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/plotly/figureFactory.py b/src/feelpp/benchmarking/json_report/figures/plotly/figureFactory.py new file mode 100644 index 000000000..75c3709ec --- /dev/null +++ b/src/feelpp/benchmarking/json_report/figures/plotly/figureFactory.py @@ -0,0 +1,40 @@ +import warnings +from feelpp.benchmarking.json_report.figures.plotly.plotlyFigures import * + + + +class FigureFactory: + """ Factory class to dispatch concrete figure elements""" + + @staticmethod + def create(plot_type, plot_config) -> PlotlyFigure: + """ Creates a concrete figure element + Args: + plot_config (Plot). Pydantic object with the plot configuration information + """ + if plot_type in ["scatter","marked_scatter"]: + fill_lines = ["optimal","half-optimal"] if plot_config.transformation=="speedup" else [] + + if plot_type == "scatter": + return PlotlyScatterFigure(plot_config, fill_lines) + elif plot_type == "marked_scatter": + return PlotlyMarkedScatter(plot_config, fill_lines) + elif plot_type == "table": + return PlotlyTableFigure(plot_config) + elif plot_type == "stacked_bar": + return PlotlyStackedBarFigure(plot_config) + elif plot_type == "grouped_bar": + return PlotlyGroupedBarFigure(plot_config) + elif plot_type == "heatmap": + return PlotlyHeatmapFigure(plot_config) + elif plot_type == "sunburst": + return PlotlySunburstFigure(plot_config) + elif plot_type == "scatter3d": + return PlotlyScatter3DFigure(plot_config) + elif plot_type == "surface3d": + return PlotlySurface3DFigure(plot_config) + elif plot_type == "parallelcoordinates": + return PlotlyParallelcoordinatesFigure(plot_config) + else: + warnings.warn(f"Figure type note implemented {plot_type}") + return None \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/plotlyFigures.py b/src/feelpp/benchmarking/json_report/figures/plotly/plotlyFigures.py similarity index 83% rename from src/feelpp/benchmarking/json_report/figures/plotlyFigures.py rename to src/feelpp/benchmarking/json_report/figures/plotly/plotlyFigures.py index 239fa27c1..d718fd3ad 100644 --- a/src/feelpp/benchmarking/json_report/figures/plotlyFigures.py +++ b/src/feelpp/benchmarking/json_report/figures/plotly/plotlyFigures.py @@ -8,8 +8,11 @@ class PlotlyFigure(Figure): """ Base class for a Plotly figure """ - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) + + def createJson(self,df): + return self.createFigure(df).to_json() def createTraces(self,df): raise NotImplementedError("Pure virtual function. Not to be called from the base class") @@ -37,7 +40,7 @@ def createSliderAnimation(self,df): """ frames = [] ranges=[] - secondary_axis = self.transformation_strategy.dimensions["secondary_axis"].parameter + secondary_axis = self.config.secondary_axis.parameter anim_dimension_values = df.index.get_level_values(secondary_axis).unique().values for dim in anim_dimension_values: @@ -65,7 +68,7 @@ def createSliderAnimation(self,df): return fig - def createMultiindexFigure(self,df): + def createMultiindexFigure(self,df,data_dirpath="."): """ Creates a plotly figure from a multiIndex dataframe Args: df (pd.DataFrame). The transformed dataframe (must be multiindex) @@ -74,7 +77,7 @@ def createMultiindexFigure(self,df): """ return self.createSliderAnimation(df) - def createSimpleFigure(self,df): + def createSimpleFigure(self,df,data_dirpath="."): """ Creates a plotly figure from a given dataframe Args: df (pd.DataFrame). The transformed dataframe @@ -83,26 +86,26 @@ def createSimpleFigure(self,df): """ return go.Figure(self.createTraces(df)) - def createFigure(self,df): + def createFigure(self,df, data_dirpath = "."): """ Creates a figure from the master dataframe Args: df (pd.DataFrame). The master dataframe containing all reframe test data Returns: go.Figure: Plotly figure corresponding to the grouped Bar type """ - figure = super().createFigure(df) + figure = super().createFigure(df,data_dirpath) figure.update_layout(self.config.layout_modifiers) figure = self.updateLayout(figure) return figure - def createHtml(self,df): - return self.createFigure(df).to_html(auto_play=False,include_plotlyjs=False, full_html=False) + def createHtml(self,df,data_dirpath="."): + return self.createFigure(df,data_dirpath).to_html(auto_play=False,include_plotlyjs=False, full_html=False) class PlotlyScatterFigure(PlotlyFigure): """ Concrete Figure class for scatter figures """ - def __init__(self, plot_config,transformation_strategy,fill_lines=[]): - super().__init__(plot_config,transformation_strategy) + def __init__(self, plot_config,fill_lines=[]): + super().__init__(plot_config) self.fill_lines = fill_lines def createTraces(self,df): @@ -121,28 +124,28 @@ def createTraces(self,df): class PlotlyMarkedScatter(PlotlyFigure): """ Concrete Figure class for marked scatter figures """ - def __init__(self, plot_config,transformation_strategy,fill_lines=[]): - super().__init__(plot_config,transformation_strategy) + def __init__(self, plot_config,fill_lines=[]): + super().__init__(plot_config) self.fill_lines = fill_lines self.marks = ["circle","square","diamond","cross","x","triangle-up","triangle-down","triangle-left","triangle-right","pentagon","hexagon","octagon","star","hexagram","star-triangle-up","star-triangle-down","star-square","star-diamond","diamond-tall","diamond-wide","hourglass","bowtie"] self.colors = ["red","blue","green","orange","purple","brown","pink","gray","cyan","magenta","yellow","darkblue","darkred","darkgreen","darkorange","darkpurple","darkbrown","darkpink","darkgray","darkcyan","darkmagenta","darkyellow","lightblue","lightred","lightgreen","lightorange","lightpurple","lightbrown","lightpink","lightgray","lightcyan","lightmagenta","lightyellow","black"] - if len(self.transformation_strategy.dimensions["extra_axes"])>0: - self.mark_axis = self.transformation_strategy.dimensions["extra_axes"][0].parameter + if len(self.config.extra_axes)>0: + self.mark_axis = self.config.extra_axes[0].parameter self.mark_axis_label = self.config.extra_axes[0].label else: - self.mark_axis = self.transformation_strategy.dimensions["secondary_axis"].parameter + self.mark_axis = self.config.secondary_axis.parameter self.mark_axis_label = self.config.secondary_axis.label if self.mark_axis else self.config.color_axis.label if self.config.color_axis else "" - def createMultiindexFigure(self, df): + def createMultiindexFigure(self, df, data_dirpath="."): if len(df.index.names) == 2: - return super().createSimpleFigure(df) + return super().createSimpleFigure(df,data_dirpath) elif len(df.index.names) == 3: - return super().createMultiindexFigure(df) + return super().createMultiindexFigure(df,data_dirpath) else: raise ValueError("Marked scatter figures can only be created from 2 or 3 level multiindex dataframes") - def createSimpleFigure(self, df): + def createSimpleFigure(self, df, data_dirpath="."): return go.Figure(self.createMarkTraces(df)) def createTraces(self,df): @@ -198,8 +201,8 @@ def updateLayout(self, fig): class PlotlyTableFigure(PlotlyFigure): """ Concrete Figure class for scatter figures """ - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) self.precision = 3 def cellFormat(self,df): @@ -211,7 +214,7 @@ def cellFormat(self,df): """ return [f'.{self.precision}' if t == float64 else '' for t in [df.index.dtype] + df.dtypes.values.tolist()] - def createMultiindexFigure(self,df): + def createMultiindexFigure(self,df,data_dirpath="."): """ Creates a plotly table from a multiindex dataframe Args: df (pd.DataFrame). The transformed dataframe (must be multiindex) @@ -228,7 +231,7 @@ def createMultiindexFigure(self,df): ) ) - def createSimpleFigure(self,df): + def createSimpleFigure(self,df,data_dirpath="."): """ Creates a simple plotly table from a dataframe Args: df (pd.DataFrame). The transformed dataframe @@ -253,18 +256,18 @@ def updateLayout(self, fig): class PlotlyStackedBarFigure(PlotlyFigure): """ Concrete Figure class for stacked bar charts""" - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) - def createMultiindexFigure(self,df): + def createMultiindexFigure(self,df,data_dirpath="."): """ Creates a stacked and grouped plotly bar chart from a multiindex dataframe Args: df (pd.DataFrame). The transformed dataframe (must be multiindex) Returns: go.Figure. Containing a stacked and grouped bar traces for a multiindex dataframe """ - xaxis = self.transformation_strategy.dimensions["xaxis"].parameter - secondary = self.transformation_strategy.dimensions["secondary_axis"].parameter + xaxis = self.config.xaxis.parameter + secondary = self.config.secondary_axis.parameter df2 = df.reset_index() @@ -278,7 +281,7 @@ def createMultiindexFigure(self,df): fig.update_layout(barmode="stack") return fig - def createSimpleFigure(self,df): + def createSimpleFigure(self,df,data_dirpath="."): """ Creates a stacked plotly bar chart from a single indexed dataframe Args: df (pd.DataFrame). The transformed dataframe @@ -304,8 +307,8 @@ def updateLayout(self,fig): class PlotlyGroupedBarFigure(PlotlyFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) def createTraces(self,df): """ Creates the Bar traces for a given dataframe. Useful for animation creation. @@ -319,8 +322,8 @@ def createTraces(self,df): ] class PlotlyHeatmapFigure(PlotlyFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) def createTraces(self, df): """ Creates the Heatmap traces for a given dataframe. Useful for animation creation. @@ -348,10 +351,10 @@ def updateLayout(self, fig): class PlotlySunburstFigure(PlotlyFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) - def createMultiindexFigure(self, df): + def createMultiindexFigure(self, df,data_dirpath="."): """ Creates the Sunburst traces for a given dataframe. Useful for animation creation. Args: - df (pd.DataFrame): The dataframe containing the figure data. @@ -363,7 +366,7 @@ def createMultiindexFigure(self, df): values = "value" ) - def createSimpleFigure(self, df): + def createSimpleFigure(self, df, data_dirpath="."): """ Creates a Plotly Sunburst figure from a given dataframe Args: df (pd.DataFrame). The transformed dataframe @@ -384,8 +387,8 @@ def updateLayout(self, fig): return fig class PlotlyParallelcoordinatesFigure(PlotlyFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) @staticmethod def encodeFactorize(df): @@ -395,7 +398,7 @@ def encodeFactorize(df): melted_factorized[column],_ = pd_factorize(df[column]) return melted_factorized - def createSimpleFigure(self, df): + def createSimpleFigure(self, df,data_dirpath="."): melted = df.reset_index().melt(value_vars=df.columns, id_vars=df.index.name) melted_factorized = self.encodeFactorize(melted) @@ -411,7 +414,7 @@ def createSimpleFigure(self, df): ) ) - def createMultiindexFigure(self, df): + def createMultiindexFigure(self, df,data_dirpath="."): melted = df.reset_index().melt(value_vars=df.columns,id_vars=df.index.names) melted_factorized = self.encodeFactorize(melted) @@ -437,24 +440,24 @@ def updateLayout(self, fig): class Plotly3DFigure(PlotlyFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) - if len(self.transformation_strategy.dimensions["extra_axes"])>0: - self.y_axis = self.transformation_strategy.dimensions["extra_axes"][0].parameter + def __init__(self, plot_config): + super().__init__(plot_config) + if len(self.config.extra_axes)>0: + self.y_axis = self.config.extra_axes[0].parameter self.y_axis_label = self.config.extra_axes[0].label else: - self.y_axis = self.transformation_strategy.dimensions["secondary_axis"].parameter + self.y_axis = self.config.secondary_axis.parameter self.y_axis_label = self.config.secondary_axis.label if self.y_axis else self.config.color_axis.label if self.config.color_axis else "" - def createMultiindexFigure(self, df): + def createMultiindexFigure(self, df, data_dirpath="."): if len(df.index.names) == 2: - return super().createSimpleFigure(df) #3D simple figure is equivalent to a multiindex 2D figure + return super().createSimpleFigure(df,data_dirpath) #3D simple figure is equivalent to a multiindex 2D figure elif len(df.index.names) == 3: - return super().createMultiindexFigure(df) + return super().createMultiindexFigure(df,data_dirpath) else: raise ValueError("3D figures can only be created from 2 or 3 level multiindex dataframes") - def createSimpleFigure(self, df): + def createSimpleFigure(self, df,data_dirpath="."): if not df.empty: raise ValueError("Secondary axis must be specified for 3d Figures") return go.Figure() @@ -473,8 +476,8 @@ def updateLayout(self, fig): return fig class PlotlyScatter3DFigure(Plotly3DFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) def createTraces(self, df): """ Creates a 3D scatter plot traces @@ -485,7 +488,7 @@ def createTraces(self, df): """ return [ go.Scatter3d( - x=df.index.get_level_values(self.transformation_strategy.dimensions["xaxis"].parameter), + x=df.index.get_level_values(self.config.xaxis.parameter), y=df.index.get_level_values(self.y_axis), z=df[col], mode='markers', name=col @@ -495,8 +498,8 @@ def createTraces(self, df): class PlotlySurface3DFigure(Plotly3DFigure): - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy) + def __init__(self, plot_config): + super().__init__(plot_config) def createTraces(self, df): """ Creates a 3D surface plot traces @@ -507,7 +510,7 @@ def createTraces(self, df): """ return [ go.Mesh3d( - x=df.index.get_level_values(self.transformation_strategy.dimensions["xaxis"].parameter), + x=df.index.get_level_values(self.config.xaxis.parameter), y=df.index.get_level_values(self.y_axis), z=df[col], opacity=0.5, name=col diff --git a/src/feelpp/benchmarking/json_report/figures/templates/tikz/groupedBarChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/templates/tikz/groupedBarChart.tex.j2 deleted file mode 100644 index 3b031fdd4..000000000 --- a/src/feelpp/benchmarking/json_report/figures/templates/tikz/groupedBarChart.tex.j2 +++ /dev/null @@ -1,54 +0,0 @@ -\documentclass[12pt]{article} - -\usepackage{pgf-pie} % For pie charts -\usepackage{currfile} % Required for getting the current file name -\usepackage{tikz} % Required for drawing graphics -\usepackage{pgfplots} -\usepackage{pgfplotstable} -\pgfplotsset{compat=newest} -\usepackage{underscore} - -\begin{document} - -\newcommand{\plot}[2][]{ - \begin{tikzpicture} - \begin{axis}[ - width=\textwidth, height=0.6172\textwidth, - xlabel={ {{xaxis.label}} }, ylabel={ {{yaxis.label}} }, - xtick=data, xtick align=outside, - xticklabels from table={{'{#2}'}}{{'{'}}{{xaxis.parameter}}{{'}'}}, - ymajorgrids=true, yminorgrids=true, - bar width=7pt, - cycle list name=color list, - ybar, - legend style={at={(0.5,-0.1)},anchor=north} - ] - {% for var,name in zip(variables,names) %} - \addplot table [x expr=\coordindex, y={{ var }}] {{'{#2}'}} ; - \addlegendentry{ {{name}} } - {% endfor %} - - \end{axis} - \end{tikzpicture} -} - - -{% for fn in csv_filenames %} -\pgfplotstableread[col sep=comma]{{'{'}}{{fn}}{{'}'}}\data{{loop.index | inttouniquestr }} -{% endfor %} - - -\begin{figure} - -{% if anim_dimension_values %} - {% for dim in anim_dimension_values %} - \plot{\data{{ loop.index | inttouniquestr }}} - \caption{ {{secondary_axis.label}}={{dim}} } - {% endfor %} -{% else %} - \plot{\data{{1 | inttouniquestr }}} -{% endif %} -\caption{ {{caption}} } -\end{figure} - -\end{document} diff --git a/src/feelpp/benchmarking/json_report/figures/templates/tikz/scatterChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/templates/tikz/scatterChart.tex.j2 deleted file mode 100644 index dc0d831b5..000000000 --- a/src/feelpp/benchmarking/json_report/figures/templates/tikz/scatterChart.tex.j2 +++ /dev/null @@ -1,62 +0,0 @@ -\documentclass[12pt]{article} - -\usepackage{pgf-pie} % For pie charts -\usepackage{currfile} % Required for getting the current file name -\usepackage{tikz} % Required for drawing graphics -\usepackage{pgfplots} -\usepackage{pgfplotstable} -\pgfplotsset{compat=newest} -\usepgfplotslibrary{fillbetween} -\usepackage{underscore} - -\begin{document} - -\newcommand{\plot}[2][]{ - \begin{tikzpicture} - \begin{axis}[ - width=\textwidth, height=0.6172\textwidth, - xlabel={ {{xaxis.label}} }, ylabel={ {{yaxis.label}} }, - xtick=data, xtick align=outside, - ymajorgrids=true, yminorgrids=true, - xticklabels from table={{'{#2}'}}{{'{'}}{{xaxis.parameter}}{{'}'}}, - cycle list name=color list, legend style={at={(0.5,-0.1)},anchor=north} - ] - {% for var,name in zip(variables,names) %} - {% if var not in fill_lines %} - \addplot table [x expr=\coordindex, y={{ var }}] {{'{#2}'}} ; - \addlegendentry{ {{name}} } - {% endif %} - {% endfor %} - - {% for fill_col in fill_lines %} - \addplot[black, name path={{fill_col}}] table [x expr=\coordindex, y={{ fill_col }}] {{'{#2}'}} ; - \addlegendentry{ {{fill_col}} } - {% endfor %} - {% if fill_lines %} - \addplot[black, fill opacity=0.2] fill between[of={{fill_lines[0]}} and {{fill_lines[-1]}}]; - {% endif %} - - \end{axis} - \end{tikzpicture} -} - - -{% for fn in csv_filenames %} -\pgfplotstableread[col sep=comma]{{'{'}}{{fn}}{{'}'}}\data{{loop.index | inttouniquestr }} -{% endfor %} - - -\begin{figure} - -{% if anim_dimension_values %} - {% for dim in anim_dimension_values %} - \plot{\data{{ loop.index | inttouniquestr }}} - \caption{ {{secondary_axis.label}}={{dim}} } - {% endfor %} -{% else %} - \plot{\data{{1 | inttouniquestr }}} -{% endif %} -\caption{ {{caption}} } -\end{figure} - -\end{document} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/templates/tikz/tableChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/templates/tikz/tableChart.tex.j2 deleted file mode 100644 index 3999b24d4..000000000 --- a/src/feelpp/benchmarking/json_report/figures/templates/tikz/tableChart.tex.j2 +++ /dev/null @@ -1,31 +0,0 @@ -\documentclass[12pt]{article} - -\usepackage{pgf-pie} % For pie charts -\usepackage{currfile} % Required for getting the current file name -\usepackage{tikz} % Required for drawing graphics -\usepackage{pgfplots} -\usepackage{pgfplotstable} -\pgfplotsset{compat=newest} -\usepackage{underscore} - -\begin{document} - -{% for fn in csv_filenames %} -\pgfplotstableread[col sep=comma]{{'{'}}{{fn}}{{'}'}}\data{{loop.index | inttouniquestr }} -{% endfor %} - - -\begin{table} - -{% if anim_dimension_values %} - {% for dim in anim_dimension_values %} - \pgfplotstabletypeset[]{\data{{ loop.index | inttouniquestr }}} - \caption{ {{secondary_axis.label}}={{dim}} } - {% endfor %} -{% else %} - \pgfplotstabletypeset[]{\data{{1 | inttouniquestr }}} -{% endif %} -\caption{ {{caption}} } -\end{table} - -\end{document} diff --git a/src/feelpp/benchmarking/json_report/figures/tikz/figureFactory.py b/src/feelpp/benchmarking/json_report/figures/tikz/figureFactory.py new file mode 100644 index 000000000..a19edf0e4 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/figures/tikz/figureFactory.py @@ -0,0 +1,25 @@ +import warnings +from feelpp.benchmarking.json_report.figures.tikz.tikzFigures import TikzFigure, TikzScatterFigure, TikzGroupedBarFigure, TikzStackedBarFigure, TikzTableFigure + +class FigureFactory: + """ Factory class to dispatch concrete figure elements""" + @staticmethod + def create(plot_type,plot_config) -> TikzFigure: + """ Creates a concrete figure element + Args: + plot_config (Plot). Pydantic object with the plot configuration information + """ + if plot_type in ["scatter","marked_scatter"]: + fill_lines = ["optimal","half-optimal"] if plot_config.transformation=="speedup" else [] + + if plot_type == "scatter": + return TikzScatterFigure(plot_config, fill_lines) + elif plot_type == "table": + return TikzTableFigure(plot_config) + elif plot_type == "stacked_bar": + return TikzStackedBarFigure(plot_config) + elif plot_type == "grouped_bar": + return TikzGroupedBarFigure(plot_config) + else: + warnings.warn(f"Figure type note implemented {plot_type}") + return None \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/tikz/templates/groupedBarChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/tikz/templates/groupedBarChart.tex.j2 new file mode 100644 index 000000000..aaa4f79f0 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/figures/tikz/templates/groupedBarChart.tex.j2 @@ -0,0 +1,59 @@ +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=0.6172\textwidth, + xlabel={ {{xaxis.label}} }, ylabel={ {{yaxis.label}} }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + xticklabels from table={{'{#2}'}}{{'{'}}{{xaxis.parameter}}{{'}'}}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + bar width=7pt, + cycle list name=color list, + ybar, + legend to name=customlegend, + legend style={font=\small} + ] + {% for col in columns %} + \addplot table [x expr=\coordindex, y={{ col }}] {{'{#2}'}} ; + \addlegendentry{ {{col}} } + {% endfor %} + + \end{axis} + \end{tikzpicture} +} + + +{% for fn in csv_filenames %} +\pgfplotstableread[col sep=comma]{{'{\\detokenize{'}}{{fn}}{{'}}'}}\data{{loop.index | inttouniquestr }} +{% endfor %} + + +\begin{figure}[H] + \centering + {% if anim_dimension_values %} + {% set n = anim_dimension_values | length %} + {% set ncols = (n**0.5) | round(0, 'ceil') %} + {% set nrows = (n / ncols) | round(0, 'ceil') %} + {% set subfig_width = ((0.95 / ncols) | round(3)) %} + + {% for dim in anim_dimension_values %} + \begin{subfigure}[t]{ {{subfig_width}}\textwidth} + \centering + \plot{\data{{ loop.index | inttouniquestr }}} + \caption{ {{secondary_axis.label}}={{dim}} } + \end{subfigure} + {% if (loop.index0 + 1) % ncols == 0 %} + \par\vspace{1em} + {% endif %} + {% endfor %} + {% else %} + \plot{\data{{1 | inttouniquestr }}} + {% endif %} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ {{caption}} } +\end{figure} diff --git a/src/feelpp/benchmarking/json_report/figures/tikz/templates/scatterChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/tikz/templates/scatterChart.tex.j2 new file mode 100644 index 000000000..606e5dd41 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/figures/tikz/templates/scatterChart.tex.j2 @@ -0,0 +1,67 @@ +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=\linewidth, + xlabel={ {{xaxis.label}} }, ylabel={ {{yaxis.label}} }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + xticklabels from table={{'{#2}'}}{{'{'}}{{xaxis.parameter}}{{'}'}}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + cycle list name=color list, + legend to name=customlegend, + legend style={font=\small} + ] + {% for col in columns %} + {% if col not in fill_lines %} + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y={{ col }}] {{'{#2}'}} ; + \addlegendentry{ {{col}} } + {% endif %} + {% endfor %} + + {% for fill_col in fill_lines %} + \addplot[black, name path={{fill_col}}] table [x expr=\coordindex, y={{ fill_col }}] {{'{#2}'}} ; + \addlegendentry{ {{fill_col}} } + {% endfor %} + {% if fill_lines %} + \addplot[black, fill opacity=0.2] fill between[of={{fill_lines[0]}} and {{fill_lines[-1]}}]; + {% endif %} + + \end{axis} + \end{tikzpicture} +} + + +{% for fn in csv_filenames %} +\pgfplotstableread[col sep=comma]{{'{\\detokenize{'}}{{fn}}{{'}}'}}\data{{loop.index | inttouniquestr }} +{% endfor %} + + +\begin{figure}[H] + \centering + {% if anim_dimension_values %} + {% set n = anim_dimension_values | length %} + {% set ncols = (n**0.5) | round(0, 'ceil') %} + {% set nrows = (n / ncols) | round(0, 'ceil') %} + {% set subfig_width = ((0.95 / ncols) | round(3)) %} + + {% for dim in anim_dimension_values %} + \begin{subfigure}[t]{ {{subfig_width}}\textwidth} % adjust width as needed + \centering + \plot{\data{{ loop.index | inttouniquestr }}} + \caption{ {{secondary_axis.label}}={{dim}} } + \end{subfigure} + {% if (loop.index0 + 1) % ncols == 0 %} + \par\vspace{1em} + {% endif %} + {% endfor %} + {% else %} + \plot{\data{{1 | inttouniquestr }}} + {% endif %} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ {{caption}} } +\end{figure} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/templates/tikz/stackedBarChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/tikz/templates/stackedBarChart.tex.j2 similarity index 60% rename from src/feelpp/benchmarking/json_report/figures/templates/tikz/stackedBarChart.tex.j2 rename to src/feelpp/benchmarking/json_report/figures/tikz/templates/stackedBarChart.tex.j2 index 13eced053..4dd536b43 100644 --- a/src/feelpp/benchmarking/json_report/figures/templates/tikz/stackedBarChart.tex.j2 +++ b/src/feelpp/benchmarking/json_report/figures/tikz/templates/stackedBarChart.tex.j2 @@ -1,28 +1,17 @@ -\documentclass[12pt]{article} - -\usepackage{pgf-pie} % For pie charts -\usepackage{currfile} % Required for getting the current file name -\usepackage{tikz} % Required for drawing graphics -\usepackage{pgfplots} -\usepackage{pgfplotstable} -\pgfplotsset{compat=newest} -\usepackage{underscore} \makeatletter -\newcommand\resetstackedplots{ +\DeclareRobustCommand\resetstackedplots{ \makeatletter \pgfplots@stacked@isfirstplottrue \makeatother } -\begin{document} - {% for fn in csv_filenames %} -\pgfplotstableread[col sep=comma]{{'{'}}{{fn}}{{'}'}}\data{{loop.index | inttouniquestr }} +\pgfplotstableread[col sep=comma]{{'{\\detokenize{'}}{{fn}}{{'}}'}}\data{{loop.index | inttouniquestr }} {% endfor %} -\begin{figure} +\begin{figure}[H] \begin{tikzpicture} \begin{axis}[ @@ -41,10 +30,10 @@ {% if i > 1 %} \resetstackedplots {% endif %} - {% for var,name,color in zip(variables,names, colors) %} - \addplot+[ybar, bar width=0.2,point meta=y,draw=black,fill={{color}}{% if i > 1 %}, forget plot{% endif %} ] table [x expr=\coordindex+0.25*{{i}}, y={{ var }}] {\data{{i | inttouniquestr}}} ; + {% for col,color in zip(columns, colors) %} + \addplot+[ybar, bar width=0.2,point meta=y,draw=black,fill={{color}}{% if i > 1 %}, forget plot{% endif %} ] table [x expr=\coordindex+0.25*{{i}}, y={{ col }}] {\data{{i | inttouniquestr}}} ; {% if i == 1%} - \addlegendentry{ {{name}} } + \addlegendentry{ {{col}} } {% endif %} {% endfor %} {% endfor %} @@ -54,5 +43,3 @@ \caption{ {{caption}} } \end{figure} - -\end{document} diff --git a/src/feelpp/benchmarking/json_report/figures/tikz/templates/tableChart.tex.j2 b/src/feelpp/benchmarking/json_report/figures/tikz/templates/tableChart.tex.j2 new file mode 100644 index 000000000..cc26822d2 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/figures/tikz/templates/tableChart.tex.j2 @@ -0,0 +1,19 @@ + + +{% for fn in csv_filenames %} +\pgfplotstableread[col sep=comma]{{'{\\detokenize{'}}{{fn}}{{'}}'}}\data{{loop.index | inttouniquestr }} +{% endfor %} + + +\begin{table}[H] + +{% if anim_dimension_values %} + {% for dim in anim_dimension_values %} + \pgfplotstabletypeset[]{\data{{ loop.index | inttouniquestr }}} + \caption{ {{secondary_axis.label}}={{dim}} } + {% endfor %} +{% else %} + \pgfplotstabletypeset[]{\data{{1 | inttouniquestr }}} +{% endif %} +\caption{ {{caption}} } +\end{table} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/figures/tikzFigures.py b/src/feelpp/benchmarking/json_report/figures/tikz/tikzFigures.py similarity index 67% rename from src/feelpp/benchmarking/json_report/figures/tikzFigures.py rename to src/feelpp/benchmarking/json_report/figures/tikz/tikzFigures.py index ea8fb682a..9937dc1c8 100644 --- a/src/feelpp/benchmarking/json_report/figures/tikzFigures.py +++ b/src/feelpp/benchmarking/json_report/figures/tikz/tikzFigures.py @@ -1,3 +1,4 @@ +import os from jinja2 import Environment, FileSystemLoader from feelpp.benchmarking.json_report.figures.base import Figure @@ -26,34 +27,36 @@ def intToUniqueStr(n): class TikzFigure(Figure): """Base class for Tikz figures""" - def __init__(self,plot_config, transformation_strategy, renderer_filename): - super().__init__(plot_config, transformation_strategy) - self.template_dirpath = f"{Path(__file__).resolve().parent}/templates/tikz/" + def __init__(self,plot_config, renderer_filename): + super().__init__(plot_config) + self.template_dirpath = f"{Path(__file__).resolve().parent}/templates/" self.renderer = Renderer(self.template_dirpath,renderer_filename) self.xcolors = ["red","green","blue","magenta","yellow","black","gray","white","darkgray","lightgray","olive","orange","pink","purple","teal","violet","cyan","brown","lime"] - def createMultiindexFigure(self, df, **args): + def createMultiindexFigure(self, df, data_dirpath, **args): """ Creates a latex tikz (pgfplots) figure from a multiIndex dataframe Args: df (pd.DataFrame). The transformed dataframe (must be multiindex) Returns: str: latex file content where containing multiple pgfplots figures, for each value of secondary axis """ - secondary_axis = self.transformation_strategy.dimensions["secondary_axis"].parameter + secondary_axis = self.config.secondary_axis.parameter anim_dim_values = df.index.get_level_values(secondary_axis).unique().values return self.renderer.template.render( xaxis = self.config.xaxis, yaxis = self.config.yaxis, caption = self.config.title, + color_axis = self.config.color_axis, + columns = df.columns.tolist(), secondary_axis = self.config.secondary_axis, anim_dimension_values = [str(dim) for dim in anim_dim_values], - csv_filenames = [f"{dim}.csv" for dim in anim_dim_values], + csv_filenames = [os.path.join(data_dirpath,self.sanitizeFilename(f"{dim}.csv")) for dim in anim_dim_values], **args ) - def createSimpleFigure(self, df, **args): + def createSimpleFigure(self, df,data_dirpath, **args): """ Creates a latex tikz (pgfplots) figure from a given dataframe Args: df (pd.DataFrame). The transformed dataframe @@ -63,35 +66,37 @@ def createSimpleFigure(self, df, **args): return self.renderer.template.render( xaxis = self.config.xaxis, yaxis = self.config.yaxis, + color_axis = self.config.color_axis, caption = self.config.title, - csv_filenames = [f"{self.config.title}.csv"], + columns = df.columns.tolist(), + csv_filenames = [os.path.join(data_dirpath,self.sanitizeFilename(f"{self.config.title}.csv"))], **args ) class TikzScatterFigure(TikzFigure): """ Concrete Figure class for pgfplots scatter figure""" - def __init__(self, plot_config, transformation_strategy, fill_lines = []): - super().__init__(plot_config, transformation_strategy, "scatterChart.tex.j2") + def __init__(self, plot_config, fill_lines = []): + super().__init__(plot_config, "scatterChart.tex.j2") self.fill_lines = fill_lines - def createFigure(self, df): - return super().createFigure(df, fill_lines = self.fill_lines) + def createFigure(self, df, data_dir): + return super().createFigure(df, data_dir, fill_lines = self.fill_lines) class TikzTableFigure(TikzFigure): """ Concrete Figure class for pgfplots table""" - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy, "tableChart.tex.j2") + def __init__(self, plot_config): + super().__init__(plot_config, "tableChart.tex.j2") class TikzStackedBarFigure(TikzFigure): """ Concrete Figure class for pgfplots stacked bar figure""" - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy, "stackedBarChart.tex.j2") + def __init__(self, plot_config): + super().__init__(plot_config, "stackedBarChart.tex.j2") - def createFigure(self,df): - return super().createFigure(df, colors=self.xcolors[:len(df.columns)]) + def createFigure(self,df, data_dir): + return super().createFigure(df,data_dir, colors=self.xcolors[:len(df.columns)]) class TikzGroupedBarFigure(TikzFigure): """ Concrete Figure class for pgfplots grouped bar figure""" - def __init__(self, plot_config, transformation_strategy): - super().__init__(plot_config, transformation_strategy, "groupedBarChart.tex.j2") + def __init__(self, plot_config): + super().__init__(plot_config, "groupedBarChart.tex.j2") diff --git a/src/feelpp/benchmarking/json_report/figures/transformationFactory.py b/src/feelpp/benchmarking/json_report/figures/transformationFactory.py index 2f49b578d..498b0a6a4 100644 --- a/src/feelpp/benchmarking/json_report/figures/transformationFactory.py +++ b/src/feelpp/benchmarking/json_report/figures/transformationFactory.py @@ -162,7 +162,7 @@ def calculate(self,df): pivot["half-optimal"] = (pivot["optimal"] -1) / 2 + 1 return pivot -class TransformationStrategyFactory: +class TransformationFactory: """ Factory class to dispatch concrete transformation strategies""" @staticmethod def create(plot_config): diff --git a/src/feelpp/benchmarking/json_report/renderer.py b/src/feelpp/benchmarking/json_report/renderer.py index 12f00ea35..339bdc214 100644 --- a/src/feelpp/benchmarking/json_report/renderer.py +++ b/src/feelpp/benchmarking/json_report/renderer.py @@ -1,5 +1,5 @@ -import json, os, warnings -import pandas as pd +import json, os, warnings, tempfile, shutil +from uuid import uuid4 from feelpp.benchmarking.jsonWithComments import JSONWithCommentsDecoder from feelpp.benchmarking.json_report.schemas.jsonReport import JsonReportSchema @@ -18,6 +18,23 @@ def __init__(self, report_filepath: str, output_format:str = "adoc") -> None: self.exposed:dict = dict() self.data:dict = self.loadReportData() + self.id = uuid4().hex + + + @classmethod + def initFromLoaded( cls, id, report: JsonReportSchema, report_data:dict, output_format:str="adoc" )->"JsonReportController": + json_report_ctrl = cls.__new__(cls) + json_report_ctrl.id = id + json_report_ctrl.data = report_data + json_report_ctrl.report = report + json_report_ctrl.output_format = output_format + json_report_ctrl.report_filepath = None + + json_report_ctrl.exposed = {} + + json_report_ctrl.renderer = json_report_ctrl.initRenderer() + + return json_report_ctrl def loadReport( self, report_filepath: str ) -> JsonReportSchema: if not os.path.exists( report_filepath ): @@ -33,16 +50,20 @@ def getTemplatePath( self ): template_filename = None if self.output_format == "adoc": template_filename = "json2adoc_report.adoc.j2" + elif self.output_format == "tex": + template_filename = "json2tex_report.tex.j2" #TODO: add more formats here (latex,html,...) else: raise ValueError(f"Output format '{self.output_format}' not supported.") - return os.path.join(os.path.dirname(__file__),"templates"), template_filename + return os.path.join(os.path.dirname(__file__),"templates",self.output_format), template_filename def initRenderer( self) -> TemplateRenderer: template_path, template_filename = self.getTemplatePath( ) renderer = TemplateRenderer( template_paths=template_path, template_filename=template_filename ) renderer.env.globals.update( { + "zip":zip, + "JsonReportController":JsonReportController, "FiguresController":FiguresController, "TableController":TableController, "TextController":TextController @@ -65,7 +86,7 @@ def loadReportData( self ): return data - def render(self, output_dirpath: str, output_filename:str = None ) -> str: + def render(self, output_dirpath: str, output_filename:str = None, attachments_dirpath:str=None, **kwargs ) -> str: if not os.path.exists( output_dirpath ): os.makedirs( output_dirpath ) @@ -74,6 +95,32 @@ def render(self, output_dirpath: str, output_filename:str = None ) -> str: output_filepath = os.path.join( output_dirpath, os.path.basename(output_filename) ) - self.renderer.render( output_filepath, dict(report=self.report, report_data = self.data )) - - return os.path.abspath(output_filepath) \ No newline at end of file + if not attachments_dirpath: + attachments_dirpath = os.path.join(os.path.dirname(output_filepath),"data") + + self.renderer.render( + output_filepath, + dict( + uuid = self.id, + report=self.report, + report_data = self.data, + attachments_dirpath = attachments_dirpath, + **kwargs + ) + ) + + return os.path.abspath(output_filepath) + + def exportAsZip( self, output_dirpath: str, output_filename:str = None, **kwargs ) -> str: + with tempfile.TemporaryDirectory() as tmpdir: + report_path = self.render( + output_dirpath=tmpdir, + output_filename=f"{output_filename}.{self.output_format}", + attachments_dirpath=os.path.join(tmpdir,"data"), + attachments_base_url = "./data", + **kwargs + ) + + shutil.make_archive(os.path.join(output_dirpath,output_filename),"zip",tmpdir) + + return os.path.join(output_dirpath,output_filename) \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/schemas/jsonReport.py b/src/feelpp/benchmarking/json_report/schemas/jsonReport.py index 07fe90c00..56722a7ec 100644 --- a/src/feelpp/benchmarking/json_report/schemas/jsonReport.py +++ b/src/feelpp/benchmarking/json_report/schemas/jsonReport.py @@ -1,3 +1,4 @@ +import requests, warnings,os from typing import Literal, Union, Optional, List, Dict, Annotated, Callable, Any from pydantic import ValidationError, BaseModel, field_validator, model_validator, Field, ConfigDict from datetime import datetime @@ -8,6 +9,7 @@ class ReportNode(BaseModel): type:str + id: Optional[str] = None ref: Optional[str] = None model_config = ConfigDict( extra="forbid" ) @@ -19,6 +21,7 @@ class TextNode(ReportNode): class LatexNode(ReportNode): type:Literal["latex"] + is_equation: Optional[bool] = False latex: str class ImageNode(ReportNode): @@ -27,9 +30,29 @@ class ImageNode(ReportNode): caption: Optional[str] = None alt: Optional[str] = None style: Optional[List[str]] = ["img-fluid"] + is_remote: Optional[bool] = True + + + def downloadImage(self,dirpath:str=".") -> str: + if not self.is_remote: + warnings.warn("downloadImage() used for local image... Will return the src attr") + return self.src + + response = requests.get(self.src) + image_name = os.path.basename(self.src) + if response.status_code != 200: + warnings.warn(f"Could not download image from {self.src}") + return image_name + + os.makedirs(dirpath,exist_ok=True) + image_path = os.path.join(dirpath,image_name) + with open(image_path,"wb") as f: + f.write(response.content) + return image_name class PlotNode(ReportNode): type: Literal["plot"] + caption: Optional[str] = None plot: Plot @@ -68,6 +91,7 @@ class SectionNode(ReportNode): class GridNode(ReportNode): type: Literal["grid"] contents: Optional[List[Node]] = [] + caption: Optional[str] = None columns: Optional[int] = 1 justify: Optional[Literal["start","center","end"]] = "start" align: Optional[Literal["start","center","end"]] = "start" diff --git a/src/feelpp/benchmarking/json_report/tables/controller.py b/src/feelpp/benchmarking/json_report/tables/controller.py index 6ae5ae2a9..1d42eff3b 100644 --- a/src/feelpp/benchmarking/json_report/tables/controller.py +++ b/src/feelpp/benchmarking/json_report/tables/controller.py @@ -25,13 +25,24 @@ def generate(self) -> pd.DataFrame: return df - def getColsAlignment(self) -> str: + def getColsAlignment(self,format="adoc") -> str: """ Generate the AsciiDoc cols="..." string based on column_align in style. Defaults to 'left' if not specified. Returns a string like: "c,l,r,l,r" """ - align_map = {"left": "<", "center": "^", "right": ">"} + if format=="adoc": + align_map = {"left": "<", "center": "^", "right": ">"} + default_align = "<" + separator="," + formatter = lambda align,width : f"{align}{width}" + elif format=="tex": + align_map = {"left": "l", "center": "c", "right": "r"} + default_align = "l" + separator=" " + formatter = lambda align,width : f"{align}" + else: + raise NotImplementedError(f"Format not recognized during table column alignment extraction.. : {format}") cols = [] column_align = self.style.column_align @@ -39,9 +50,9 @@ def getColsAlignment(self) -> str: for col in self.layout.column_order or []: a = column_align.get(col) w = column_width.get(col,3) - cols.append(f"{align_map.get(a,'<')}{w}") + cols.append(formatter(align_map.get(a,default_align),w)) - return ",".join(cols) + return separator.join(cols) def _renameColumns(self, df: pd.DataFrame) -> pd.DataFrame: if self.layout.rename: diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/json2adoc_report.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/json2adoc_report.adoc.j2 new file mode 100644 index 000000000..a48831afc --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/json2adoc_report.adoc.j2 @@ -0,0 +1,25 @@ +:example-caption: Figure +{% if report.description %} +:description: {{ report.description }} +{% endif %} +{% if report.datetime %} +:docdatetime: {{ report.datetime }} +{% endif %} + + +{% set context = { + "uuid": uuid, + "attachments_dirpath": attachments_dirpath, + "attachments_base_url": attachments_base_url +} %} + +{% import "macros/section.adoc.j2" as section %} +{{ section.render(report, report_data, 1, context=context) }} + +{% if include_latex_download %} +{% set json_report_ctrl = JsonReportController.initFromLoaded(uuid, report, report_data, output_format="tex" ) %} +{% set rendered_latex_path = json_report_ctrl.exportAsZip(attachments_dirpath~"/"~uuid,"report" ) %} +++++ + LaTeX +++++ +{% endif %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/grid.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/grid.adoc.j2 new file mode 100644 index 000000000..f0105a2ed --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/grid.adoc.j2 @@ -0,0 +1,38 @@ +{% import "macros/image.adoc.j2" as image %} +{% import "macros/plot.adoc.j2" as plot %} +{% import "macros/latex.adoc.j2" as latex %} +{% import "macros/text.adoc.j2" as text %} +{% import "macros/table.adoc.j2" as table %} +{% import "macros/itemize.adoc.j2" as itemize %} + +{% macro render(gridNode,report_data, context={}) %} +{% if gridNode.id %} +[[{{gridNode.id}}]] +{% endif %} +[example.grid] +{% if gridNode.caption %} +.{{gridNode.caption}} +{% endif %} +==== +[.grid.grid-{{gridNode.columns}}.gap-{{gridNode.gap}}.justify-{{gridNode.justify}}.align-{{gridNode.align}}] +-- +:figure-caption!: +{%for node in gridNode.contents%} +{% if node.type == "image" %} +{{ image.render(node,report_data.get(node.ref), make_block=False, context=context) }} +{% elif node.type == "plot" %} +{{ plot.render(node,report_data.get(node.ref), make_block=False, context=context) }} +{% elif node.type == "latex" %} +{{ latex.render(node,report_data.get(node.ref), context=context) }} +{% elif node.type == "text" %} +{{ text.render(node,report_data.get(node.ref), context=context) }} +{% elif node.type == "table" %} +{{ table.render(node,report_data.get(node.ref), context=context) }} +{% elif node.type == "itemize" %} +{{ itemize.render(node,report_data.get(node.ref), context=context) }} +{% endif %} +{% endfor %} +:figure-caption: Figure +-- +==== +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/image.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/image.adoc.j2 new file mode 100644 index 000000000..a9619f7df --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/image.adoc.j2 @@ -0,0 +1,20 @@ +{% macro render(imageNode,data,make_block = True, context={}) %} +{% set src = imageNode.src %} +{% set alt = imageNode.alt or imageNode.caption or "" %} +{% set styles = imageNode.style | join(' ')%} + +{% if imageNode.id %} +[[{{imageNode.id}}]] +{% endif %} +{% if imageNode.caption %} +.{{imageNode.caption}} +{% endif %} +{% if make_block %} +[example.image] +==== +image::{{src}}[role="{{styles}}",alt={{alt}}] +==== +{% else %} +image::{{src}}[role="{{styles}}",alt={{alt}}] +{% endif %} +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/itemize.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/itemize.adoc.j2 new file mode 100644 index 000000000..fc30938c8 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/itemize.adoc.j2 @@ -0,0 +1,12 @@ +{% import "macros/text.adoc.j2" as text %} + +{% macro render(listNode,data,context={}) %} + +{% if listNode.id %} +[[{{listNode.id}}]] +{% endif %} +{% for item in listNode.items %} +- {{text.render(item,data,context=context)}} +{% endfor %} + +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/latex.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/latex.adoc.j2 new file mode 100644 index 000000000..6bc512afc --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/latex.adoc.j2 @@ -0,0 +1,17 @@ +{% macro render(latexNode,data,context={}) %} + +{% if latexNode.id %} +[[{{latexNode.id}}]] +{% endif %} +[stem] +++++ +{%if latexNode.is_equation %} +\begin{equation} +{{ latexNode.latex }} +\end{equation} +{% else %} +{{ latexNode.latex }} +{% endif %} +++++ + +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/plot.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/plot.adoc.j2 new file mode 100644 index 000000000..cbf1a25b2 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/plot.adoc.j2 @@ -0,0 +1,69 @@ +{% macro render(plotNode,data, make_block=True, context={}) %} + + +{% if plotNode.id %} +[[{{plotNode.id}}]] +{% endif %} +{% if make_block %} +[example.plot] +{%if plotNode.caption %} +.{{plotNode.caption}} +{% endif %} +==== +{% endif %} +++++ + +{% set figure_ctrl = FiguresController(data.get(plotNode.ref),plotNode.plot, context.uuid) %} + +
+ {% if figure_ctrl.figure_views | length > 1%} +
+ {% for plot_type in figure_ctrl.figure_views.keys() %} + + {% endfor %} +
+ {% endif %} + + + {% for plot_type in figure_ctrl.figure_views.keys() %} + {% set exported_paths = figure_ctrl.exportFigureData( + plot_type = plot_type, + backend = "plotly", + transformation = plotNode.plot.transformation, + formats = ["zip_csv","json"], + outdir = context.attachments_dirpath + )%} + {% set csv_url = (context.attachments_base_url or context.attachments_dirpath)~"/"~exported_paths["zip_csv"] %} + {% set json_url = (context.attachments_base_url or context.attachments_dirpath)~"/"~exported_paths["json"] %} + {% set figure_div_id = "graph-div-"~figure_ctrl.id~"-"~loop.index%} + +
+
+
CSV
+
+ + + + {% endfor %} + + +
+ + + +{%if plotNode.caption and make_block==False %} +
{{plotNode.caption}}
+{% endif %} + + +++++ +{% if make_block %} +==== +{% endif %} + +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/macros/section.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/section.adoc.j2 similarity index 73% rename from src/feelpp/benchmarking/json_report/templates/macros/section.adoc.j2 rename to src/feelpp/benchmarking/json_report/templates/adoc/macros/section.adoc.j2 index d04566a27..cbfb819f8 100644 --- a/src/feelpp/benchmarking/json_report/templates/macros/section.adoc.j2 +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/section.adoc.j2 @@ -15,21 +15,24 @@ "itemize": itemize.render } %} -{% macro render(sectionNode, report_data, level=1) %} +{% macro render(sectionNode, report_data, level=1, context={}) %} +{% if sectionNode.id %} +[[{{sectionNode.id}}]] +{% endif %} {%if sectionNode.title %} {{ '=' * level }} {{ sectionNode.title }} {%endif%} {% for node in sectionNode.contents %} {% if node.type == "section" %} -{{ render(node, report_data, level + 1) }} +{{ render(node, report_data, level + 1,context=context) }} {% elif node.type == "grid" %} -{{ grid.render(node, report_data ) }} +{{ grid.render(node, report_data,context=context) }} {% else %} {% set handler = handlers.get(node.type) %} {% if handler %} -{{ handler(node,report_data.get(node.ref)) }} +{{ handler(node,report_data.get(node.ref),context=context) }} {% endif %} {% endif %} {% endfor %} diff --git a/src/feelpp/benchmarking/json_report/templates/macros/table.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/table.adoc.j2 similarity index 91% rename from src/feelpp/benchmarking/json_report/templates/macros/table.adoc.j2 rename to src/feelpp/benchmarking/json_report/templates/adoc/macros/table.adoc.j2 index 9d22dd8b9..647d4f669 100644 --- a/src/feelpp/benchmarking/json_report/templates/macros/table.adoc.j2 +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/table.adoc.j2 @@ -1,16 +1,19 @@ -{% macro render(tableNode, data) %} +{% macro render(tableNode, data, context={}) %} {%set table_controller = TableController(data.get(tableNode.ref),tableNode.layout, tableNode.style) %} {%set table = table_controller.generate() %} -{% if tableNode.caption %} -.{{tableNode.caption}} -{% endif %} {% if tableNode.style.classnames %} [{% for classname in tableNode.style.classnames %}.{{classname}}{% endfor %}] {% endif %} -- +{% if tableNode.id %} +[[{{tableNode.id}}]] +{% endif %} +{% if tableNode.caption %} +.{{tableNode.caption}} +{% endif %} [cols="{{ table_controller.getColsAlignment() }}",options="header"] |=== diff --git a/src/feelpp/benchmarking/json_report/templates/adoc/macros/text.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/adoc/macros/text.adoc.j2 new file mode 100644 index 000000000..bd0ff6f74 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/adoc/macros/text.adoc.j2 @@ -0,0 +1,6 @@ +{% macro render(textNode,data,context={}) %} +{%if textNode.id %} +[[{{textNode.id}}]] +{% endif %} +{{ TextController((data or {}).get(textNode.ref),textNode.text).generate() }} +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/json2adoc_report.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/json2adoc_report.adoc.j2 deleted file mode 100644 index 0340d76b6..000000000 --- a/src/feelpp/benchmarking/json_report/templates/json2adoc_report.adoc.j2 +++ /dev/null @@ -1,10 +0,0 @@ -{% if report.description %} -:description: {{ report.description }} -{% endif %} -{% if report.datetime %} -:docdatetime: {{ report.datetime }} -{% endif %} - -{% import "macros/section.adoc.j2" as section %} - -{{ section.render(report, report_data, 1) }} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/macros/grid.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/macros/grid.adoc.j2 deleted file mode 100644 index 42db4b9bd..000000000 --- a/src/feelpp/benchmarking/json_report/templates/macros/grid.adoc.j2 +++ /dev/null @@ -1,27 +0,0 @@ -{% import "macros/image.adoc.j2" as image %} -{% import "macros/plot.adoc.j2" as plot %} -{% import "macros/latex.adoc.j2" as latex %} -{% import "macros/text.adoc.j2" as text %} -{% import "macros/table.adoc.j2" as table %} -{% import "macros/itemize.adoc.j2" as itemize %} - -{% set handlers = { - "plot": plot.render, - "text": text.render, - "image": image.render, - "latex": latex.render, - "table": table.render, - "itemize": itemize.render -} %} - -{% macro render(gridNode,report_data) %} -[.grid.grid-{{gridNode.columns}}.grid-{{gridNode.gap}}.justify-{{gridNode.justify}}.align-{{gridNode.align}}] --- -{%for node in gridNode.contents%} -{% set handler = handlers.get(node.type) %} -{% if handler %} -{{ handler(node,report_data.get(node.ref)) }} -{% endif %} -{% endfor %} --- -{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/macros/image.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/macros/image.adoc.j2 deleted file mode 100644 index 88c221a09..000000000 --- a/src/feelpp/benchmarking/json_report/templates/macros/image.adoc.j2 +++ /dev/null @@ -1,12 +0,0 @@ -{% macro render(imageNode,data) %} -{% set src = imageNode.src %} -{% set alt = imageNode.alt or imageNode.caption or "" %} -{% set styles = imageNode.style | join(' ')%} - -{% if imageNode.caption %} -image::{{src}}[role="{{styles}}",alt={{alt}},title={{imageNode.caption}}] -{% else %} -image::{{src}}[role=""{{styles}}"",alt={{alt}}] -{% endif %} - -{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/macros/itemize.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/macros/itemize.adoc.j2 deleted file mode 100644 index 25e2d2e2a..000000000 --- a/src/feelpp/benchmarking/json_report/templates/macros/itemize.adoc.j2 +++ /dev/null @@ -1,9 +0,0 @@ -{% import "macros/text.adoc.j2" as text %} - -{% macro render(listNode,data) %} - -{% for item in listNode.items %} -- {{text.render(item,data)}} -{% endfor %} - -{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/macros/latex.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/macros/latex.adoc.j2 deleted file mode 100644 index cdfebdf1e..000000000 --- a/src/feelpp/benchmarking/json_report/templates/macros/latex.adoc.j2 +++ /dev/null @@ -1,8 +0,0 @@ -{% macro render(latexNode,data) %} - -[stem] -++++ -{{ latexNode.latex }} -++++ - -{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/macros/plot.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/macros/plot.adoc.j2 deleted file mode 100644 index 8660519bf..000000000 --- a/src/feelpp/benchmarking/json_report/templates/macros/plot.adoc.j2 +++ /dev/null @@ -1,36 +0,0 @@ -{% macro render(plotNode,data) %} - -++++ - -{% for figure in FiguresController(data.get(plotNode.ref),plotNode.plot).generateAll() %} -
- {% if figure.plot_types | length > 1%} -
- {% for plot_type in figure.plot_types %} - {% set plot_type_i = loop.index %} - - {% endfor %} -
- {% endif %} - - {% for subfigure in figure.subfigures %} -
- -
- {% for export in subfigure.exports %} - - {% endfor %} -
- - {{subfigure.html}} -
- {% endfor %} -
- -{% endfor %} - -++++ - -{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/tex/json2tex_report.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/json2tex_report.tex.j2 new file mode 100644 index 000000000..84d3d5aa4 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/json2tex_report.tex.j2 @@ -0,0 +1,38 @@ +{% set context = { + "uuid":uuid, + "attachments_dirpath": attachments_dirpath, + "attachments_base_url": attachments_base_url, +} %} + +\documentclass[11pt]{report} + +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} + +{% for package in report.extra_packages %} +\usepackage{{"{"}}{{package}}{{"}"}} +{% endfor %} +\pgfplotsset{compat=newest} + +\begin{document} + +{% import "macros/section.tex.j2" as section %} + +{{ section.render(report, report_data, 1,context=context) }} + +\end{document} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/grid.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/grid.tex.j2 new file mode 100644 index 000000000..37be2949d --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/grid.tex.j2 @@ -0,0 +1,43 @@ +{% import "macros/image.tex.j2" as image %} +{% import "macros/table.tex.j2" as table %} +{% import "macros/latex.tex.j2" as latex %} +{% import "macros/text.tex.j2" as text %} +{% import "macros/itemize.tex.j2" as itemize %} + +{% macro render(gridNode, report_data,context={}) %} + +{% set col_width = 1.0 / gridNode.columns %} + +\begin{figure}[ht!] +\centering + +{% for node in gridNode.contents %} +\begin{minipage}[t]{ {{ "%.3f"|format(col_width) }}\linewidth } + +{% if node.type == "image" %} +{{ image.render(node, report_data.get(node.ref), make_block=False,context=context) }} +{% elif node.type == "latex" %} +{{ latex.render(node, report_data.get(node.ref),context=context) }} +{% elif node.type == "text" %} +{{ text.render(node, report_data.get(node.ref),context=context) }} +{% elif node.type == "table" %} +{{ table.render(node, report_data.get(node.ref),context=context) }} +{% elif node.type == "itemize" %} +{{ itemize.render(node, report_data.get(node.ref),context=context) }} +{% endif %} +\end{minipage}% +{% if not loop.last %} +\hfill +{% endif %} +{% endfor %} + +{% if gridNode.caption %} +\caption{ {{ gridNode.caption }} } +{% endif %} +{% if gridNode.id %} +\label{fig:{{ gridNode.id }}} +{% endif %} + +\end{figure} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/image.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/image.tex.j2 new file mode 100644 index 000000000..83b13ffd3 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/image.tex.j2 @@ -0,0 +1,36 @@ +{% macro includeGraphics(imageNode,styles,context={}) %} +{% set img_filename = imageNode.downloadImage(context.attachments_dirpath~"/"~context.uuid) %} +{% set img_url = (context.attachments_base_url or context.attachments_dirpath)~"/"~context.uuid~"/"~img_filename %} +{% if 'img-fluid' in styles %} +\includegraphics[width=\linewidth]{ {{img_url}} } +{% else %} +\includegraphics{ {{img_url}} } +{% endif %} +{% endmacro %} + +{% macro render(imageNode, data, make_block = True,context={}) %} +{% set caption = imageNode.caption or "" %} +{% set alt = imageNode.alt or "" %} +{% set styles = imageNode.style or [] %} +{% if make_block %} +\begin{figure}[ht!] + \centering + {{-includeGraphics(imageNode,styles,context = context)-}} + {% if caption %} + \caption{ {{ caption }} } + {% endif %} + {% if alt %} + % Alt text: {{ alt }} + {% endif %} + {% if imageNode.id %} + \label{ {{imageNode.id}} } + {% endif %} +\end{figure} +{%else%} +{{-includeGraphics(imageNode,styles,context = context)-}} +{% if caption %}\subcaption{{'{'}}{{caption}}{{'}'}}{% endif %} +{% if imageNode.id %} +\label{ {{imageNode.id}} } +{% endif %} +{% endif %} +{% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/itemize.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/itemize.tex.j2 new file mode 100644 index 000000000..dc04b681b --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/itemize.tex.j2 @@ -0,0 +1,11 @@ +{% import "macros/text.tex.j2" as text %} + +{% macro render(listNode, data,context={}) %} + +\begin{itemize} +{% for item in listNode.items %} + \item {{ text.render(item, data,context=context) }} +{% endfor %} +\end{itemize} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/latex.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/latex.tex.j2 new file mode 100644 index 000000000..75541d556 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/latex.tex.j2 @@ -0,0 +1,5 @@ +{% macro render(latexNode, data,context={}) %} + +{{ latexNode.latex }} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/plot.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/plot.tex.j2 new file mode 100644 index 000000000..3879ada85 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/plot.tex.j2 @@ -0,0 +1,27 @@ +{% macro render(plotNode,data,context={}) %} + +{% set figure_ctrl = FiguresController(data.get(plotNode.ref),plotNode.plot, context.uuid) %} + +{# CURRENTLY ONLY THE FIRST PLOT TYPE IS EXPORTED #} +{% set plot_type = plotNode.plot.plot_types[0] %} +{% set exported_paths = figure_ctrl.exportFigureData( + plot_type = plot_type, + backend = "latex", + transformation = plotNode.plot.transformation, + formats = ["csv"], + outdir = context.attachments_dirpath +)%} +{% set data_url = (context.attachments_base_url or context.attachments_dirpath)~"/"~exported_paths["csv"] %} + +{%set fig = figure_ctrl.renderFigure( + plot_type = plot_type, + backend = "latex", + transformation = plotNode.plot.transformation, + data_dir = data_url +) %} + +{%if fig %} +{{fig}} +{% endif %} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/section.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/section.tex.j2 new file mode 100644 index 000000000..b2df9567c --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/section.tex.j2 @@ -0,0 +1,44 @@ +{% import "macros/text.tex.j2" as text %} +{% import "macros/table.tex.j2" as table %} +{% import "macros/image.tex.j2" as image %} +{% import "macros/plot.tex.j2" as plot %} +{% import "macros/latex.tex.j2" as latex %} +{% import "macros/itemize.tex.j2" as itemize %} +{% import "macros/grid.tex.j2" as grid %} + +{% set handlers = { + "plot": plot.render, + "text": text.render, + "image": image.render, + "latex": latex.render, + "table": table.render, + "itemize": itemize.render +} %} + +{% set levelTag = [ "\\chapter","\\section","\\subsection" ]%} + +{% macro render(sectionNode, report_data, level=1, context={}) %} + +{% if level - 1 < levelTag | length %} +{{levelTag[ level -1 ]}}{{'{'}}{% if sectionNode.title %}{{sectionNode.title}}{% endif %}{{'}'}} +{% else %} +\paragraph{{'{'}} {% if sectionNode.title %}{{sectionNode.title}}{% endif %} {{'}'}} +{% endif %} +{%if sectionNode.id %} +\label{{'{'}}{{sectionNode.id}}{{'}'}} +{% endif %} + +{% for node in sectionNode.contents %} +{% if node.type == "section" %} +{{ render(node, report_data, level + 1, context=context) }} +{% elif node.type == "grid" %} +{{ grid.render(node, report_data, context=context) }} +{% else %} +{% set handler = handlers.get(node.type) %} +{% if handler %} +{{ handler(node, report_data.get(node.ref),context=context) }} +{% endif %} +{% endif %} +{% endfor %} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/tex/macros/table.tex.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/table.tex.j2 new file mode 100644 index 000000000..61ab89348 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/table.tex.j2 @@ -0,0 +1,44 @@ +{% import "macros/text.tex.j2" as text %} + +{% macro escapeSpecialChars(text) -%} +{{ text | string + | replace('\\', '\textbackslash{}') + | replace('{', '\\{') + | replace('}', '\\}') + | replace('#', '\\#') + | replace('$', '\\$') + | replace('%', '\\%') + | replace('&', '\\&') + | replace('_', '\\_') + | replace('~', '\\textasciitilde{}') + | replace('^', '\\^{}') +}} +{%- endmacro %} + +{% macro render(tableNode, data, context={}) %} + +{% set table_controller = TableController(data.get(tableNode.ref), tableNode.layout, tableNode.style) %} +{% set table = table_controller.generate() %} +{% set cols_align = table_controller.getColsAlignment(format="tex") %} + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ {{ cols_align }} } + \hline + {% for col in table.columns %}{{ escapeSpecialChars(col) }}{% if not loop.last %} & {% endif %}{% endfor %} \\ + \hline + {% for row in table.itertuples(index=False) %} + {% for cell in row %}{{ escapeSpecialChars(cell) }}{% if not loop.last %} & {% endif %}{% endfor %} \\ + {% endfor %} + \hline + \end{tabular}} + {% if tableNode.caption %} + \caption{ {{ tableNode.caption }} } + {% endif %} + {% if tableNode.id %} + \label{{'{'}}{{tableNode.id}}{{'}'}} + {% endif %} +\end{table} + +{% endmacro %} diff --git a/src/feelpp/benchmarking/json_report/templates/macros/text.adoc.j2 b/src/feelpp/benchmarking/json_report/templates/tex/macros/text.tex.j2 similarity index 52% rename from src/feelpp/benchmarking/json_report/templates/macros/text.adoc.j2 rename to src/feelpp/benchmarking/json_report/templates/tex/macros/text.tex.j2 index 37eeda3f2..cf771a6d4 100644 --- a/src/feelpp/benchmarking/json_report/templates/macros/text.adoc.j2 +++ b/src/feelpp/benchmarking/json_report/templates/tex/macros/text.tex.j2 @@ -1,3 +1,3 @@ -{% macro render(textNode,data) %} -{{ TextController((data or {}).get(textNode.ref),textNode.text).generate() }} +{% macro render(textNode,data,context={}) %} +{{ TextController((data or {}).get(textNode.ref),textNode.text).generate(format="tex") }} {% endmacro %} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.adoc index 855535a60..0eafa3059 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.adoc @@ -1,3 +1,4 @@ +:example-caption: Figure :docdatetime: == Introduction diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.tex new file mode 100644 index 000000000..0ad15f7d6 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/basic_text.tex @@ -0,0 +1,80 @@ +\documentclass[11pt]{report} +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} + +\pgfplotsset{compat=newest} + +\begin{document} + +\chapter{} + +\section{Introduction} + +This is a basic end-to-end test to verify that plain text and sections render correctly. + +\subsection{Subsection} + +Nested sections should appear with correct formatting. + +\section{Raw Object} + +Custom from file : SOME RAW TEST + +Custom from inline : Some data i want to refactor across nodes + +\section{Object from file} + +Number of students 2 + +\begin{itemize} + \item Javier: 100 (B) + \item Oscar: 90 (C) +\end{itemize} + +\section{Inline Object} + +Value my data value + +\section{References} + +Unchanged reference : SOME RAW TEST + +Reference lower : some raw test + +Inline ref : Some data i want to refactor across nodes + +Inline UPPER : SOME DATA I WANT TO REFACTOR ACROSS NODES + + +\begin{itemize} + \item Javier Copy: 100 (B) + + \item Oscar Copy: 90 (C) + +\end{itemize} + + +Dumped json : {"students": {"javier": {"score": "100", "grade": "B"}, "oscar": {"score": "90", "grade": "C"}}} + +Inline object from ref: my data value + +Object Casted to raw: {"data1": "my data value"} + + + +\end{document} diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.adoc index 2586d7ebc..8992f2fa5 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.adoc @@ -1,3 +1,4 @@ +:example-caption: Figure :docdatetime: == Metadata diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.tex new file mode 100644 index 000000000..3465e693a --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/list_with_data.tex @@ -0,0 +1,35 @@ +\documentclass[11pt]{report} + +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} + +\pgfplotsset{compat=newest} + +\begin{document} + +\chapter{} + +\section{Metadata} + +\begin{itemize} + \item Hostname: machine-01 + \item Author: tester + \item Version: 1.0 +\end{itemize} + +\end{document} diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.adoc index 6c080f5b0..5e6cb0915 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.adoc @@ -1,27 +1,38 @@ +:example-caption: Figure :docdatetime: == Score Analysis +[example.plot] +==== ++++
-
-
- - -
-
-
FIGURE
- -
+
+
+
CSV
+ +
+ + + ++++ +==== + +[example.plot] +==== ++++
@@ -30,35 +41,37 @@
-
- -
- - -
-
-
FIGURE
- -
+
+
+
CSV
-
-
- - -
-
-
FIGURE
- -
+ + +
+
+
CSV
+ + +
++++ - +==== The scatter plot shows the average score per subject for each student, while the stacked bar plot visualizes how many subjects each student passed. diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.tex new file mode 100644 index 000000000..288678ec8 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/plot_features.tex @@ -0,0 +1,129 @@ + +\documentclass[11pt]{report} + +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} + +\pgfplotsset{compat=newest} + +\begin{document} + + +\chapter{} + + +\section{Score Analysis} + + +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=\linewidth, + xlabel={ Subject }, ylabel={ Average Score }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + xticklabels from table={#2}{subject}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + cycle list name=color list, + legend to name=customlegend, + legend style={font=\small} + ] + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=Alice] {#2} ; + \addlegendentry{ Alice } + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=Bob] {#2} ; + \addlegendentry{ Bob } + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=Charlie] {#2} ; + \addlegendentry{ Charlie } + + + \end{axis} + \end{tikzpicture} +} + + +\pgfplotstableread[col sep=comma]{\detokenize{}}\dataA + + +\begin{figure}[H] + \centering + \plot{\dataA} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ Scores by Subject } +\end{figure} + + + + + + +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=0.6172\textwidth, + xlabel={ Subject }, ylabel={ Number of Passes }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + xticklabels from table={#2}{subject}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + bar width=7pt, + cycle list name=color list, + ybar, + legend to name=customlegend, + legend style={font=\small} + ] + \addplot table [x expr=\coordindex, y=Alice] {#2} ; + \addlegendentry{ Alice } + \addplot table [x expr=\coordindex, y=Bob] {#2} ; + \addlegendentry{ Bob } + \addplot table [x expr=\coordindex, y=Charlie] {#2} ; + \addlegendentry{ Charlie } + + \end{axis} + \end{tikzpicture} +} + + +\pgfplotstableread[col sep=comma]{\detokenize{}}\dataA + + +\begin{figure}[H] + \centering + \plot{\dataA} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ Pass Rate per Subject } +\end{figure} + + +The scatter plot shows the average score per subject for each student, while the stacked bar plot visualizes how many subjects each student passed. + + + + + + +\end{document} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.adoc index 307df8f72..6b7c4d16c 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.adoc @@ -1,3 +1,4 @@ +:example-caption: Figure :docdatetime: Processed Text: Foo Foo Baz diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.tex new file mode 100644 index 000000000..f9535aa1e --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/preprocessor.tex @@ -0,0 +1,27 @@ +\documentclass[11pt]{report} +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} +\pgfplotsset{compat=newest} + +\begin{document} + +\chapter{} + +Processed Text: Foo Foo Baz + +\end{document} diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/report.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/report.adoc index 9b462f10f..ee6bbdc6d 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/report.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/report.adoc @@ -1,3 +1,4 @@ +:example-caption: Figure :docdatetime: @@ -94,44 +95,50 @@ Placeholders in this report allow dynamic insertion of values from the datasets. == Performance Analysis +[example.plot] +==== ++++
-
-
- - -
-
-
FIGURE
- -
+
+
+
CSV
+ +
++++ +==== - +[example.plot] +==== ++++
-
-
- - -
-
-
FIGURE
- -
+
+
+
CSV
+ +
++++ - +==== The scatter plot shows average scores per subject, while the bar chart shows average attendance rates for each student. diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/report.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/report.tex new file mode 100644 index 000000000..207e24b09 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/report.tex @@ -0,0 +1,163 @@ +\documentclass[11pt]{report} + +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} +\pgfplotsset{compat=newest} + + +\begin{document} + +\chapter{} + +\section{Report Overview} + +This report analyzes students' performance, attendance, and remarks across multiple subjects. We summarize scores, compute averages, and visualize trends. + +Placeholders in this report allow dynamic insertion of values from the datasets. For example, total students: 9. + +\section{Student Table} + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l r c c } + \hline +name & subject \\ + \hline +ALICE & Math \\ +BOB & Math \\ +CHARLIE & Math \\ +ALICE & Physics \\ +BOB & Physics \\ +CHARLIE & Physics \\ +ALICE & Chemistry \\ +BOB & Chemistry \\ +CHARLIE & Chemistry \\ + \hline + \end{tabular}} +\end{table} + +\section{Attendance} + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l l l } + \hline +name & subject \\ + \hline +Alice & Math \\ +Bob & Math \\ +Charlie & Math \\ +Alice & Physics \\ +Bob & Physics \\ +Charlie & Physics \\ +Alice & Chemistry \\ +Bob & Chemistry \\ +Charlie & Chemistry \\ + \hline + \end{tabular}} +\end{table} + +\section{Performance Analysis} + +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=\linewidth, + xlabel={ Subject }, ylabel={ Average Score }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + xticklabels from table={#2}{subject}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + cycle list name=color list, + legend to name=customlegend, + legend style={font=\small} + ] + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=ALICE] {#2} ; + \addlegendentry{ ALICE } + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=BOB] {#2} ; + \addlegendentry{ BOB } + \addplot+[mark=*,mark size=0.7pt] table [x expr=\coordindex, y=CHARLIE] {#2} ; + \addlegendentry{ CHARLIE } + + + \end{axis} + \end{tikzpicture} +} + +\pgfplotstableread[col sep=comma]{\detokenize{}}\dataA + +\begin{figure}[H] + \centering + \plot{\dataA} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ Average Scores by Subject } +\end{figure} + +\DeclareRobustCommand{\plot}[2][]{ + \begin{tikzpicture} + \begin{axis}[ + width=0.95\textwidth, height=0.6172\textwidth, + xlabel={ Student }, ylabel={ Attendance (%) }, + xtick=data, xtick align=outside, + ylabel style={font=\scriptsize}, + xlabel style={font=\scriptsize}, + xticklabels from table={#2}{name}, + xticklabel style={rotate=45, anchor=east, font=\scriptsize}, + yticklabel style={font=\scriptsize}, + ymajorgrids=true, yminorgrids=true, + bar width=7pt, + cycle list name=color list, + ybar, + legend to name=customlegend, + legend style={font=\small} + ] + \addplot table [x expr=\coordindex, y=Chemistry] {#2} ; + \addlegendentry{ Chemistry } + \addplot table [x expr=\coordindex, y=Math] {#2} ; + \addlegendentry{ Math } + \addplot table [x expr=\coordindex, y=Physics] {#2} ; + \addlegendentry{ Physics } + + \end{axis} + \end{tikzpicture} +} + +\pgfplotstableread[col sep=comma]{\detokenize{}}\dataA + +\begin{figure}[H] + \centering + \plot{\dataA} + \vspace{1em} + \centering + \pgfplotslegendfromname{customlegend} + \caption{ Attendance Rate by Student } +\end{figure} + +The scatter plot shows average scores per subject, while the bar chart shows average attendance rates for each student. + +\section{Teacher Notes} + +Teacher remarks (loaded from a raw text file with preprocessing applied): Excellent performance by Alice in most subjects. Bob shows consistent improvement. Charlie needs attention in Math but is improving in Physics and Chemistry. + +\end{document} diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.adoc b/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.adoc index 80d5f4e20..c70e2a35c 100644 --- a/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.adoc +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.adoc @@ -1,3 +1,4 @@ +:example-caption: Figure :docdatetime: == Student Performance diff --git a/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.tex b/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.tex new file mode 100644 index 000000000..b4afabf3d --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/data/golden/table_features.tex @@ -0,0 +1,230 @@ +\documentclass[11pt]{report} + +\usepackage{graphicx} +\usepackage{pgf-pie} +\usepackage{currfile} +\usepackage{tikz} +\usepackage{pgfplots} +\usepackage{pgfplotstable} +\usepackage{underscore} +\usepackage{amsfonts} +\usepackage{hyperref} +\usepackage{xurl} +\usepackage{subcaption} +\usepackage{amsmath} +\usepackage{float} +\usepackage[parfill]{parskip} + +\setlength{\parindent}{0pt} +\usepgfplotslibrary{fillbetween} +\pgfplotsset{compat=newest} + +\begin{document} + + +\chapter{} + +\section{Student Performance} + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l c c } + \hline +Student & Subject & Score & Grade & Passed \\ + \hline +Alice & Math & 85.0 & B & ✔️ \\ +Bob & Math & 78.0 & C & ✔️ \\ +Charlie & Math & 62.0 & D & ✔️ \\ +Alice & Physics & 92.0 & A & ✔️ \\ +Bob & Physics & 88.0 & B & ✔️ \\ +Charlie & Physics & 55.0 & D & ❌ \\ +Alice & Chemistry & 70.0 & C & ✔️ \\ +Bob & Chemistry & 65.0 & D & ✔️ \\ +Charlie & Chemistry & 45.0 & D & ❌ \\ + \hline + \end{tabular}} +\end{table} + +\section{Inline Table} + +\subsection{Default inline table} + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l l l } + \hline +time & power & temperature & comfort & power\_temp\_sum \\ + \hline +1.2 & 10 & 22.5 & 0.5 & 32.5 \\ +2.3 & 20 & 23.0 & 0.6 & 43.0 \\ +3.4 & 30 & 21.3 & 0.7 & 51.3 \\ +4.5 & 40 & 22.1 & 0.8 & 62.1 \\ + \hline + \end{tabular}} +\end{table} + + +\subsection{Styled inline table} + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ c l l l l } + \hline +time & power & temperature & power\_temp\_sum & comfort \\ + \hline +1.2 & 10 & 22.5 & 32.5 & 0.5 \\ +2.3 & 20 & 23.0 & 43.0 & 0.6 \\ +3.4 & 30 & 21.3 & 51.3 & 0.7 \\ +4.5 & 40 & 22.1 & 62.1 & 0.8 \\ + \hline + \end{tabular}} +\end{table} + + + + +\subsection{Ref to inline} + + + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l l l } + \hline +time & power & temperature & comfort & power\_temp\_sum \\ + \hline +2.3 & 20 & 23.0 & 0.6 & 43.0 \\ +3.4 & 30 & 21.3 & 0.7 & 51.3 \\ +4.5 & 40 & 22.1 & 0.8 & 62.1 \\ + \hline + \end{tabular}} +\end{table} + + + + + + + +\section{File Table} + + +\subsection{Default file table} + + + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l l l } + \hline +Scenario & Time\_s & Memory\_MB & result & memory\_per\_time \\ + \hline +Run A & 12 & 1024 & 🟢 & 81.92 \\ +Run B & 25 & 2048 & 🔴 & 81.59 \\ +Run C & 15 & 1536 & 🟢 & 96.60 \\ + \hline + \end{tabular}} +\end{table} + + + + + +\subsection{Styled file table} + + + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ c l l } + \hline +result & Scenario & memory\_per\_time \\ + \hline +🟢 & Run A & 81.92 \\ +🔴 & Run B & 81.59 \\ +🟢 & Run C & 96.60 \\ + \hline + \end{tabular}} +\end{table} + + + +\subsection{Ref to file table} + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l } + \hline +result & Scenario & Memory\_MB \\ + \hline +🔴 & 1 & 2048 \\ +🟢 & 2 & 2560 \\ + \hline + \end{tabular}} +\end{table} + + + +\section{Table from Json} + + +\subsection{Default table from json} + + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l } + \hline +subject & days\_absent & days\_present \\ + \hline +Chemistry & 4.3 & 15.7 \\ +Math & 4.7 & 15.3 \\ +Physics & 4.7 & 15.3 \\ + \hline + \end{tabular}} +\end{table} + + +\subsection{Styled table from json} + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l r r } + \hline +subject & Absents (Days) & Present (Days) \\ + \hline +Chemistry & 4.3 & 15.7 \\ +Math & 4.7 & 15.3 \\ +Physics & 4.7 & 15.3 \\ + \hline + \end{tabular}} +\end{table} + + +\subsection{Ref to json table} + + +\begin{table}[H] + \centering + \resizebox{\textwidth}{!}{\begin{tabular}{ l l l } + \hline +subject & days\_absent & days\_present \\ + \hline +Math & 4.7 & 15.3 \\ +Physics & 4.7 & 15.3 \\ +Chemistry & 4.3 & 15.7 \\ + \hline + \end{tabular}} +\end{table} + + + + +\end{document} \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/tests/figures/test_figureFactory.py b/src/feelpp/benchmarking/json_report/tests/figures/test_figureFactory.py deleted file mode 100644 index 139672b67..000000000 --- a/src/feelpp/benchmarking/json_report/tests/figures/test_figureFactory.py +++ /dev/null @@ -1,33 +0,0 @@ -from feelpp.benchmarking.json_report.figures.figureFactory import FigureFactory, ScatterFigure, TableFigure, StackedBarFigure, GroupedBarFigure, HeatmapFigure, SunburstFigure, Scatter3DFigure, Surface3DFigure, ParallelcoordinatesFigure, MarkedScatterFigure -from test_transformationFactory import PlotConfigMocker -import pytest - - - -@pytest.mark.parametrize(("types","expected_classes"),[ - (["scatter"],[ScatterFigure]), - (["scatter","table"],[ScatterFigure]), - (["table"],[TableFigure]), - (["stacked_bar"],[StackedBarFigure]), - (["grouped_bar"],[GroupedBarFigure]), - (["heatmap"],[HeatmapFigure]), - (["sunburst"],[SunburstFigure]), - (["scatter3d"],[Scatter3DFigure]), - (["surface3d"],[Surface3DFigure]), - (["parallelcoordinates"],[ParallelcoordinatesFigure]), - (["marked_scatter"],[MarkedScatterFigure]), - (["unkown"],[]) -]) -def test_figureFactory(types,expected_classes): - """ Tests the correct generation of Figure objects by the FigureFactory class """ - plot_config = PlotConfigMocker(transformation="speedup",plot_types=types) - if expected_classes: - figures = FigureFactory.create(plot_config) - assert len(types) == len(figures) - for figure,expected_class in zip(figures,expected_classes): - assert isinstance(figure,expected_class) - assert hasattr(figure,"createFigure") and callable(figure.createFigure), "Figure has no createFigure method" - assert hasattr(figure,"createTex") and callable(figure.createTex), "Figure has no createTex method" - else: - with pytest.raises(NotImplementedError): - figures = FigureFactory.create(plot_config) diff --git a/src/feelpp/benchmarking/json_report/tests/figures/test_transformationFactory.py b/src/feelpp/benchmarking/json_report/tests/figures/test_transformationFactory.py index 081afa1de..67beffb00 100644 --- a/src/feelpp/benchmarking/json_report/tests/figures/test_transformationFactory.py +++ b/src/feelpp/benchmarking/json_report/tests/figures/test_transformationFactory.py @@ -1,4 +1,4 @@ -from feelpp.benchmarking.json_report.figures.transformationFactory import PerformanceStrategy, RelativePerformanceStrategy, SpeedupStrategy, TransformationStrategyFactory +from feelpp.benchmarking.json_report.figures.transformationFactory import PerformanceStrategy, RelativePerformanceStrategy, SpeedupStrategy, TransformationFactory import pytest import pandas as pd import numpy as np @@ -35,10 +35,10 @@ def __init__( ]) def test_strategyFactory(transformation,strategy): if strategy: - assert isinstance(TransformationStrategyFactory.create(PlotConfigMocker(transformation=transformation)),strategy) + assert isinstance(TransformationFactory.create(PlotConfigMocker(transformation=transformation)),strategy) else: with pytest.raises(NotImplementedError): - TransformationStrategyFactory.create(PlotConfigMocker(transformation=transformation)) + TransformationFactory.create(PlotConfigMocker(transformation=transformation)) class MockDataframe: def __init__(self,index_type): @@ -80,7 +80,7 @@ class TestSimpleStrategies: def getCalculatedDf(self,transformation): self.plot_config.transformation = transformation - calculated_df = TransformationStrategyFactory.create(self.plot_config).calculate(self.mock_data) + calculated_df = TransformationFactory.create(self.plot_config).calculate(self.mock_data) assert calculated_df.index.name == "xaxis" assert calculated_df.columns.name == "performance_variable" assert calculated_df.isna().sum().sum() == 0 @@ -136,7 +136,7 @@ class TestComplexStrategies: def getCalculatedDf(self,transformation): self.plot_config.transformation = transformation - calculated_df = TransformationStrategyFactory.create(self.plot_config).calculate(self.mock_data) + calculated_df = TransformationFactory.create(self.plot_config).calculate(self.mock_data) assert calculated_df.index.names[0] == "secondary_axis" assert calculated_df.index.names[1] == "xaxis" assert calculated_df.columns.name == "color_axis" diff --git a/src/feelpp/benchmarking/json_report/tests/images/test_imageDownload.py b/src/feelpp/benchmarking/json_report/tests/images/test_imageDownload.py new file mode 100644 index 000000000..b5a04f656 --- /dev/null +++ b/src/feelpp/benchmarking/json_report/tests/images/test_imageDownload.py @@ -0,0 +1,61 @@ +import pytest +from feelpp.benchmarking.json_report.schemas.jsonReport import ImageNode +from pydantic import ValidationError + +class TestImageNodeDownloadFeature: + """Tests for image node download functionality""" + + def test_image_node_with_id(self): + """Test ImageNode with optional id field for cross-referencing""" + node = ImageNode( + type="image", + src="https://example.com/diagram.png", + id="architecture_diagram", + caption="System Architecture" + ) + assert node.id == "architecture_diagram" + assert node.caption == "System Architecture" + + def test_image_node_remote_false_explicit(self): + """Test ImageNode with explicit is_remote=False""" + node = ImageNode(type="image", src="./images/test.png", is_remote=False) + assert node.is_remote is False + + def test_image_node_download_image_local_warning(self): + """Test that downloadImage warns when used on local image""" + node = ImageNode(type="image", src="local.png", is_remote=False) + with pytest.warns(UserWarning): + result = node.downloadImage() + assert result == "local.png" + + def test_image_node_download_image_local_returns_src(self): + """Test downloadImage returns src for local images""" + node = ImageNode(type="image", src="path/to/image.png", is_remote=False) + with pytest.warns(UserWarning): + result = node.downloadImage() + assert result == "path/to/image.png" + + def test_image_node_validation(self): + """Test ImageNode validation""" + with pytest.raises(ValidationError): + ImageNode.model_validate({"type": "image"}) + + def test_image_download(self, tmp_path): + """Test downloadImage downloads remote image to temporary directory""" + from unittest.mock import patch, MagicMock + + node = ImageNode(type="image", src="https://example.com/test.png", is_remote=True) + + # Mock the actual HTTP download to avoid network calls + mock_response = MagicMock() + mock_response.content = b"PNG_DATA_HERE" + mock_response.status_code = 200 + + with patch('requests.get', return_value=mock_response): + result = node.downloadImage(str(tmp_path)) + + assert result == "test.png" + + downloaded_file = tmp_path / "test.png" + assert downloaded_file.exists() + assert downloaded_file.read_bytes() == b"PNG_DATA_HERE" diff --git a/src/feelpp/benchmarking/json_report/tests/test_jsonReport.py b/src/feelpp/benchmarking/json_report/tests/test_jsonReport.py index 00eedfca1..a3309b98a 100644 --- a/src/feelpp/benchmarking/json_report/tests/test_jsonReport.py +++ b/src/feelpp/benchmarking/json_report/tests/test_jsonReport.py @@ -8,20 +8,27 @@ def normalizeReport(content: str) -> str: content = re.sub( r"^:docdatetime: .*?$", ":docdatetime: ", content, flags=re.MULTILINE ) content = re.sub( - r"]*?>\s*(.*?)\s*", - lambda match : f"", + r"graph-div-.*?-", + "", content, flags=re.DOTALL ) content = re.sub( - r"]*>\s*]*>
\s*\s*
", - """ -
-
FIGURE
- -
""", - content, - flags=re.DOTALL + r"CSV", + "", + content, flags=re.DOTALL ) + content = re.sub( + r"fetch\('.*?'\)", + "", + content, flags=re.DOTALL + ) + + content = re.sub( + r"\\detokenize\{[^}]*\}", + r"\\detokenize{}", + content, flags=re.DOTALL + ) + lines = [line.rstrip() for line in content.splitlines() if line.strip()] return "\n".join(lines) @@ -46,7 +53,8 @@ def assert_report_matches_golden(output_file: str, golden_file: str): @pytest.mark.parametrize("report_filename", ["basic_text.json", "list_with_data.json", "table_features.json", "plot_features.json","preprocessor.json","report.json" ] ) -def test_jsonReportCases(report_filename,output_format="adoc"): +@pytest.mark.parametrize("output_format",["adoc","tex"]) +def test_jsonReportCases(report_filename,output_format): data_dir = os.path.join(os.path.dirname(__file__),"data") controller = JsonReportController( report_filepath=os.path.join(data_dir,report_filename), @@ -54,6 +62,6 @@ def test_jsonReportCases(report_filename,output_format="adoc"): ) with tempfile.TemporaryDirectory() as tmpdir: - output_file = controller.render(output_dirpath=os.path.join(data_dir,tmpdir)) + output_file = controller.render(output_dirpath=os.path.join(data_dir,"outputs")) golden_file = os.path.join(data_dir,"golden",report_filename.replace(".json",f".{output_format}")) assert_report_matches_golden(output_file,golden_file) \ No newline at end of file diff --git a/src/feelpp/benchmarking/json_report/tests/test_jsonReportController.py b/src/feelpp/benchmarking/json_report/tests/test_jsonReportController.py index 74c235e4d..3e0b64f4b 100644 --- a/src/feelpp/benchmarking/json_report/tests/test_jsonReportController.py +++ b/src/feelpp/benchmarking/json_report/tests/test_jsonReportController.py @@ -160,6 +160,7 @@ def test_renderWritesToOutput(monkeypatch, tmp_path): fake_renderer.render = MagicMock() ctrl = JsonReportController.__new__(JsonReportController) + ctrl.id = "ABCD" ctrl.report_filepath = "report.json" ctrl.output_format = "adoc" ctrl.renderer = fake_renderer diff --git a/src/feelpp/benchmarking/json_report/tests/text/test_textController.py b/src/feelpp/benchmarking/json_report/tests/text/test_textController.py index fe611aabf..17c7e2ce9 100644 --- a/src/feelpp/benchmarking/json_report/tests/text/test_textController.py +++ b/src/feelpp/benchmarking/json_report/tests/text/test_textController.py @@ -100,3 +100,33 @@ def test_dynamic_mode_invalid_operation_ignored(self): result = ctrl.generate() # unknown op ignored, value replaced normally assert result == "Value: test" + + # ------------------------------------------------------------------ + # LaTeX formatting: escaping and conversions + # ------------------------------------------------------------------ + @pytest.mark.parametrize("text,expected", [ + (r"50% complete", r"50\% complete"), + ("This is *bold* text", "This is \\textbf{bold} text"), + ("*first* and *second*", "\\textbf{first} and \\textbf{second}"), + ("stem:[x = \\frac{1}{2}]", "$x = \\frac{1}{2}$"), + ("Math: stem:[ a^2 + b^2 ] here", "Math: $a^2 + b^2$ here"), + ("https://example.com[Site]", "\\href{https://example.com}{Site}"), + ("https://example.com[]", "\\url{https://example.com}"), + ("<>", "\\hyperlink{section_id}{Label}"), + ("<>", "\\ref{equation_1}"), + ("*Bold* with stem:[x^2] and https://s.com[link]", + "\\textbf{Bold} with $x^2$ and \\href{https://s.com}{link}"), + ]) + def test_latex_formatting_conversions(self, text, expected): + """Test LaTeX format conversions from AsciiDoc syntax""" + ctrl = Controller({}, Text(content=text)) + result = ctrl.generate(format="tex") + assert result == expected + + def test_unknown_format_raises_error(self): + """Unknown format should raise NotImplementedError""" + ctrl = Controller({}, Text(content="test")) + with pytest.raises(NotImplementedError): + ctrl.generate(format="unknown") + + diff --git a/src/feelpp/benchmarking/json_report/text/controller.py b/src/feelpp/benchmarking/json_report/text/controller.py index bce8e4d73..49cb3dd1b 100644 --- a/src/feelpp/benchmarking/json_report/text/controller.py +++ b/src/feelpp/benchmarking/json_report/text/controller.py @@ -7,7 +7,58 @@ def __init__(self, data, text:Text): self.text_config = text self.data = data - def generate(self): + @staticmethod + def _asciidoc_to_latex_urls(text: str) -> str: + pattern = re.compile(r'([(<]*)([a-zA-Z][a-zA-Z0-9+.-]*:\S+?)\[(.*?)\]([)>.,;!?:]*)') + + def repl(m): + pre, url, label, post = m.groups() + url = url.strip() + label = label.strip() + if label: + replacement = r'\href{' + url + '}{' + label + '}' + else: + replacement = r'\url{' + url + '}' + return pre + replacement + post + + return pattern.sub(repl, text) + + + def formatText(self,text:str,format = "adoc"): + if format == "adoc": + pass + elif format == "tex": + #Escape comments + text = text.replace("%","\\%") + + # stem + text = re.sub( r"stem:\[\s*(.*?)\s*\]", r"$\1$", text ) + + #italics + re.sub(r'(?]+)\s*(?:,\s*([^>]+))?\s*>>", lambda m : f"\\hyperlink{{{m.group(1)}}}{{{m.group(2)}}}" if m.group(2) else f"\\ref{{{m.group(1)}}}", text ) + + + else: + raise NotImplementedError(f"Format not recognized in text controller: {format}") + + return text + + + + + def generate(self, format="adoc"): if self.text_config.mode == "static": content = self.text_config.content @@ -23,7 +74,7 @@ def generate(self): else: content = self.text_config.content - return content + return self.formatText(str(content),format) def _resolvePlaceHolders(self, match): expr = match.group(1) diff --git a/src/feelpp/benchmarking/reframe/schemas/defaultJsonReport.py b/src/feelpp/benchmarking/reframe/schemas/defaultJsonReport.py index 6db87b007..799edb2b8 100644 --- a/src/feelpp/benchmarking/reframe/schemas/defaultJsonReport.py +++ b/src/feelpp/benchmarking/reframe/schemas/defaultJsonReport.py @@ -80,7 +80,8 @@ def applyDefaultPlots(cls, values): item = { "type": "plot", "ref":"reframe_df", - "plot": DefaultPlot.model_validate(item["plot"]) + "plot": DefaultPlot.model_validate(item["plot"]), + "caption": item.get("caption", None) } elif isinstance(item, dict) and (item.get("type") == "section" or item.get("type")=="grid"): item = cls.applyDefaultPlots(item) diff --git a/src/feelpp/benchmarking/report/__main__.py b/src/feelpp/benchmarking/report/__main__.py index e8dac265e..1ffb4050f 100644 --- a/src/feelpp/benchmarking/report/__main__.py +++ b/src/feelpp/benchmarking/report/__main__.py @@ -1,7 +1,7 @@ from feelpp.benchmarking.dashboardRenderer.core.dashboard import Dashboard from feelpp.benchmarking.report.parser import ReportArgParser -import os, subprocess +import os, subprocess, yaml import feelpp.benchmarking from feelpp.benchmarking.report.plugins.reframeReport import ReframeReportPlugin @@ -11,6 +11,40 @@ class VersionPlugin: def process(template_data): return {"feelpp_benchmarking_version":feelpp.benchmarking.__version__} + + +def extractAntoraProjectName(antora_basepath): + if not os.path.isdir(antora_basepath): + raise FileNotFoundError(f"Could not find antora base path. Recieved: {antora_basepath}") + + with open(os.path.join(antora_basepath,"site.yml"),"r") as f: + site_content = yaml.safe_load(f) + + sources = site_content['content']['sources'] + start_path = None + + for source in sources: + if "HEAD" in source.get("branches"): + start_path = source.get("start_path") + + if not start_path: + raise ImportError("Could not find the start_path of the head branch in site.yml") + + antora_yml_path = os.path.join(antora_basepath,start_path,"antora.yml") + with open(antora_yml_path,"r") as f: + antora_yml = yaml.safe_load(f) + + project_name = antora_yml.get("name") + + if not project_name: + raise ImportError(f"Could not find the name field inside in {antora_yml_path}") + + return project_name + + + + + def main_cli(): parser = ReportArgParser() @@ -24,8 +58,11 @@ def main_cli(): dashboard.print() + #MOVE ELSEWHERE + project_name = extractAntoraProjectName(parser.args.antora_basepath) + dashboard.tree.upstreamViewData(ReframeReportPlugin.aggregator) - dashboard.render(parser.args.module_path,clean=parser.args.reset_docs) + dashboard.render(parser.args.module_path,clean=parser.args.reset_docs, project_name = project_name, include_latex_download=True) if parser.args.website: os.chdir(parser.args.antora_basepath)