Skip to content

Commit 73b21fc

Browse files
committed
[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.53125"><div xmlns="http://www.w3.org/1999/xhtml" style="display: inline-block; white-space: nowrap;"><br /><b>Execute CreateHiveTableAsSelectCommand</b><br /><br /></div></foreignObject></g></g></g></g></g></g></svg> ``` ![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__=true], Append, `spark_catalog`.`default`.`t`, org.apache.hadoop.hive.ql.io.parquet.serde.ParquetHiveSerDe, org.apache.spark.sql.execution.datasources.InMemoryFileIndex(file:/Users/hzyaoqin/spark/spark-warehouse/t), [c]"]; 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 <yao@apache.org> Signed-off-by: Kent Yao <yao@apache.org>
1 parent edb2888 commit 73b21fc

File tree

4 files changed

+54
-10
lines changed

4 files changed

+54
-10
lines changed

dev/eslint.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ module.exports = {
4040
"dataTables.rowsGroup.js"
4141
],
4242
"parserOptions": {
43-
"sourceType": "module"
43+
"sourceType": "module",
44+
"ecmaVersion": "latest"
4445
}
4546
}

sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.css

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -15,35 +15,35 @@
1515
* limitations under the License.
1616
*/
1717

18-
#plan-viz-graph .label {
18+
svg g.label {
1919
font-size: 0.85rem;
2020
font-weight: normal;
2121
text-shadow: none;
2222
color: #333;
2323
}
2424

25-
#plan-viz-graph svg g.cluster rect {
25+
svg g.cluster rect {
2626
fill: #A0DFFF;
2727
stroke: #3EC0FF;
2828
stroke-width: 1px;
2929
}
3030

31-
#plan-viz-graph svg g.node rect {
31+
svg g.node rect {
3232
fill: #C3EBFF;
3333
stroke: #3EC0FF;
3434
stroke-width: 1px;
3535
}
3636

3737
/* Highlight the SparkPlan node name */
38-
#plan-viz-graph svg text :first-child:not(.stageId-and-taskId-metrics) {
38+
svg text :first-child:not(.stageId-and-taskId-metrics) {
3939
font-weight: bold;
4040
}
4141

42-
#plan-viz-graph svg text {
42+
svg text {
4343
fill: #333;
4444
}
4545

46-
#plan-viz-graph svg path {
46+
svg path {
4747
stroke: #444;
4848
stroke-width: 1.5px;
4949
}
@@ -58,19 +58,19 @@
5858
word-wrap: break-word;
5959
}
6060

61-
#plan-viz-graph svg g.node rect.selected {
61+
svg g.node rect.selected {
6262
fill: #E25A1CFF;
6363
stroke: #317EACFF;
6464
stroke-width: 2px;
6565
}
6666

67-
#plan-viz-graph svg g.node rect.linked {
67+
svg g.node rect.linked {
6868
fill: #FFC106FF;
6969
stroke: #317EACFF;
7070
stroke-width: 2px;
7171
}
7272

73-
#plan-viz-graph svg path.linked {
73+
svg path.linked {
7474
fill: #317EACFF;
7575
stroke: #317EACFF;
7676
stroke-width: 2px;

sql/core/src/main/resources/org/apache/spark/sql/execution/ui/static/spark-sql-viz.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -312,3 +312,36 @@ function collectLinks(map, key, value) {
312312
}
313313
map.get(key).add(value);
314314
}
315+
316+
function downloadPlanBlob(b, ext) {
317+
const link = document.createElement("a");
318+
link.href = URL.createObjectURL(b);
319+
link.download = `plan.${ext}`;
320+
link.click();
321+
}
322+
323+
document.getElementById("plan-viz-download-btn").addEventListener("click", async function () {
324+
const format = document.getElementById("plan-viz-format-select").value;
325+
let blob;
326+
if (format === "svg") {
327+
const svg = planVizContainer().select("svg").node().cloneNode(true);
328+
let css = "";
329+
try {
330+
css = await fetch("/static/sql/spark-sql-viz.css").then((resp) => resp.text());
331+
} catch (e) {
332+
console.error("Failed to fetch CSS for SVG download", e);
333+
}
334+
d3.select(svg).insert("style", ":first-child").text(css);
335+
const svgData = new XMLSerializer().serializeToString(svg);
336+
blob = new Blob([svgData], { type: "image/svg+xml" });
337+
} else if (format === "dot") {
338+
const dot = d3.select("#plan-viz-metadata .dot-file").text().trim();
339+
blob = new Blob([dot], { type: "text/plain" });
340+
} else if (format === "txt") {
341+
const txt = d3.select("#physical-plan-details pre").text().trim();
342+
blob = new Blob([txt], { type: "text/plain" });
343+
} else {
344+
return;
345+
}
346+
downloadPlanBlob(blob, format);
347+
});

sql/core/src/main/scala/org/apache/spark/sql/execution/ui/ExecutionPage.scala

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ class ExecutionPage(parent: SQLTab) extends WebUIPage("execution") with Logging
7575
{jobLinks(JobExecutionStatus.SUCCEEDED, "Succeeded Jobs:")}
7676
{jobLinks(JobExecutionStatus.FAILED, "Failed Jobs:")}
7777
</ul>
78+
<div id="plan-viz-download-btn-container">
79+
<select id="plan-viz-format-select">
80+
<option value="svg">SVG</option>
81+
<option value="dot">DOT</option>
82+
<option value="txt">TXT</option>
83+
</select>
84+
<label for="plan-viz-format-select">
85+
<a id="plan-viz-download-btn" class="downloadbutton">Download</a>
86+
</label>
87+
</div>
7888
</div>
7989

8090
val metrics = sqlStore.executionMetrics(executionId)

0 commit comments

Comments
 (0)