This is an automated email from the ASF dual-hosted git repository. yao pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/spark.git
The following commit(s) were added to refs/heads/master by this push: new 73b21fcd19e2 [SPARK-51629][UI] Add a download link on the ExecutionPage for svg/dot/txt format plans 73b21fcd19e2 is described below commit 73b21fcd19e21c86338fa458ba07ca5e94f57a6d Author: Kent Yao <y...@apache.org> AuthorDate: Fri Mar 28 09:40:53 2025 +0800 [SPARK-51629][UI] Add a download link on the ExecutionPage for svg/dot/txt format plans ### What changes were proposed in this pull request? This PR adds a download link to the ExecutionPage for SVG/dot/txt format plans.  ### Why are the changes needed? These downloaded assets can improve the UX for sharing/porting to papers, social media, external advanced visualization tools, e.t.c. ### Does this PR introduce _any_ user-facing change? Yes, UI changes ### How was this patch tested? - SVG ```svg <svg xmlns="http://www.w3.org/2000/svg" viewBox="-16 -16 304.046875 95.53125" width="304.046875" height="95.53125"><style>/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ .label { font-size: 0.85rem; font-weight: normal; text-shadow: none; color: #333; } svg g.cluster rect { fill: #A0DFFF; stroke: #3EC0FF; stroke-width: 1px; } svg g.node rect { fill: #C3EBFF; stroke: #3EC0FF; stroke-width: 1px; } /* Highlight the SparkPlan node name */ svg text :first-child:not(.stageId-and-taskId-metrics) { font-weight: bold; } svg text { fill: #333; } svg path { stroke: #444; stroke-width: 1.5px; } /* Breaks the long string like file path when showing tooltips */ .tooltip-inner { word-wrap:break-word; } /* Breaks the long job url list when showing Details for Query in SQL */ .job-url { word-wrap: break-word; } svg g.node rect.selected { fill: #E25A1CFF; stroke: #317EACFF; stroke-width: 2px; } svg g.node rect.linked { fill: #FFC106FF; stroke: #317EACFF; stroke-width: 2px; } svg path.linked { fill: #317EACFF; stroke: #317EACFF; stroke-width: 2px; } </style><g><g class="output"><g class="clusters"/><g class="edgePaths"/><g class="edgeLabels"/><g class="nodes"><g class="node" id="node0" transform="translate(136.0234375,31.765625)" style="opacity: 1;" data-original-title="" title=""><rect rx="5" ry="5" x="-136.0234375" y="-31.765625" width="272.046875" height="63.53125" class="label-container"/><g class="label" transform="translate(0,0)"><g transform="translate(-131.0234375,-26.765625)"><foreignObject width="262.046875" height="53. [...] ```  - DOT ```dot digraph G { 0 [id="node0" labelType="html" label="<b>Execute InsertIntoHadoopFsRelationCommand</b><br><br>task commit time: 7 ms<br>number of written files: 1<br>job commit time: 24 ms<br>number of output rows: 1<br>number of dynamic part: 0<br>written output: 468.0 B" tooltip="Execute InsertIntoHadoopFsRelationCommand file:/Users/hzyaoqin/spark/spark-warehouse/t, false, Parquet, [parquet.compression=zstd, serialization.format=1, mergeschema=false, __hive_compatible_bucketed_table_insertion__=t [...] 1 [id="node1" labelType="html" label="<br><b>WriteFiles</b><br><br>" tooltip="WriteFiles"]; subgraph cluster2 { isCluster="true"; id="cluster2"; label="WholeStageCodegen (1)\n \nduration: 158 ms"; tooltip="WholeStageCodegen (1)"; 3 [id="node3" labelType="html" label="<br><b>Project</b><br><br>" tooltip="Project [1 AS c#0]"]; 4 [id="node4" labelType="html" label="<b>Scan OneRowRelation</b><br><br>number of output rows: 1" tooltip="Scan OneRowRelation[]"]; } 1->0; 3->1; 4->3; } ``` - TXT [plan.txt](https://github.com/user-attachments/files/19480587/plan.txt) ### Was this patch authored or co-authored using generative AI tooling? no Closes #50427 from yaooqinn/SPARK-51629. Authored-by: Kent Yao <y...@apache.org> Signed-off-by: Kent Yao <y...@apache.org> --- dev/eslint.js | 3 +- .../sql/execution/ui/static/spark-sql-viz.css | 18 ++++++------ .../spark/sql/execution/ui/static/spark-sql-viz.js | 33 ++++++++++++++++++++++ .../spark/sql/execution/ui/ExecutionPage.scala | 10 +++++++ 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/dev/eslint.js b/dev/eslint.js index 24b5170b436a..abb06526fe96 100644 --- a/dev/eslint.js +++ b/dev/eslint.js @@ -40,6 +40,7 @@ module.exports = { "dataTables.rowsGroup.js" ], "parserOptions": { - "sourceType": "module" + "sourceType": "module", + "ecmaVersion": "latest" } } diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css index d6a498e93872..032957940681 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css @@ -15,35 +15,35 @@ * limitations under the License. */ -#plan-viz-graph .label { +svg g.label { font-size: 0.85rem; font-weight: normal; text-shadow: none; color: #333; } -#plan-viz-graph svg g.cluster rect { +svg g.cluster rect { fill: #A0DFFF; stroke: #3EC0FF; stroke-width: 1px; } -#plan-viz-graph svg g.node rect { +svg g.node rect { fill: #C3EBFF; stroke: #3EC0FF; stroke-width: 1px; } /* Highlight the SparkPlan node name */ -#plan-viz-graph svg text :first-child:not(.stageId-and-taskId-metrics) { +svg text :first-child:not(.stageId-and-taskId-metrics) { font-weight: bold; } -#plan-viz-graph svg text { +svg text { fill: #333; } -#plan-viz-graph svg path { +svg path { stroke: #444; stroke-width: 1.5px; } @@ -58,19 +58,19 @@ word-wrap: break-word; } -#plan-viz-graph svg g.node rect.selected { +svg g.node rect.selected { fill: #E25A1CFF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg g.node rect.linked { +svg g.node rect.linked { fill: #FFC106FF; stroke: #317EACFF; stroke-width: 2px; } -#plan-viz-graph svg path.linked { +svg path.linked { fill: #317EACFF; stroke: #317EACFF; stroke-width: 2px; diff --git a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js index d4cc45a1639a..37bae5e4b774 100644 --- a/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js +++ b/sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js @@ -312,3 +312,36 @@ function collectLinks(map, key, value) { } map.get(key).add(value); } + +function downloadPlanBlob(b, ext) { + const link = document.createElement("a"); + link.href = URL.createObjectURL(b); + link.download = `plan.${ext}`; + link.click(); +} + +document.getElementById("plan-viz-download-btn").addEventListener("click", async function () { + const format = document.getElementById("plan-viz-format-select").value; + let blob; + if (format === "svg") { + const svg = planVizContainer().select("svg").node().cloneNode(true); + let css = ""; + try { + css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text()); + } catch (e) { + console.error("Failed to fetch CSS for SVG download", e); + } + d3.select(svg).insert("style", ":first-child").text(css); + const svgData = new XMLSerializer().serializeToString(svg); + blob = new Blob([svgData], { type: "image/svg+xml" }); + } else if (format === "dot") { + const dot = d3.select("#plan-viz-metadata .dot-file").text().trim(); + blob = new Blob([dot], { type: "text/plain" }); + } else if (format === "txt") { + const txt = d3.select("#physical-plan-details pre").text().trim(); + blob = new Blob([txt], { type: "text/plain" }); + } else { + return; + } + downloadPlanBlob(blob, format); +}); diff --git a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala index bbdf9b4c4bd8..e705b5cf46ed 100644 --- a/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala +++ b/sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala @@ -75,6 +75,16 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging {jobLinks(JobExecutionStatus.SUCCEEDED, "Succeeded Jobs:")} {jobLinks(JobExecutionStatus.FAILED, "Failed Jobs:")} </ul> + <div id="plan-viz-download-btn-container"> + <select id="plan-viz-format-select"> + <option value="svg">SVG</option> + <option value="dot">DOT</option> + <option value="txt">TXT</option> + </select> + <label for="plan-viz-format-select"> + <a id="plan-viz-download-btn" class="downloadbutton">Download</a> + </label> + </div> </div> val metrics = sqlStore.executionMetrics(executionId) --------------------------------------------------------------------- To unsubscribe, e-mail: commits-unsubscr...@spark.apache.org For additional commands, e-mail: commits-h...@spark.apache.org