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.
    
![image](https://github.com/user-attachments/assets/3359ac26-b4a6-4952-9bf0-b6ac22e6e199)
    
    ### 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. [...]
    ```
    ![plan 
(4)](https://github.com/user-attachments/assets/ba9dab38-515b-4ebf-82ab-2cd35e42fe8f)
    - 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

Reply via email to