-
Notifications
You must be signed in to change notification settings - Fork 29k
[SPARK-29543][SS][UI]Structured Streaming Web UI #26201
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9b33018
1e349ac
5633431
cb2b44d
3dca76d
b8fc23c
d2095b0
389a1c8
a2aaa46
7d6c8bf
1a994b3
ecee267
60d8d9e
db1c2b2
a034548
2273f82
5104a42
32ecfc3
8c9a851
0e5166f
cb3f338
e30af98
6de18cc
596d6bb
c036cfb
c1457f0
7c47223
81f121b
b5907cc
2bb9ce1
5accc93
380aab5
be4bec8
a14aa38
f2926a7
44b0a37
b2a334e
b3bf311
a0022a7
1e35c0c
dd5ca20
b8a4891
b3697a2
cb1e68c
2d5f66c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,171 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| // pre-define some colors for legends. | ||
| var colorPool = ["#F8C471", "#F39C12", "#B9770E", "#73C6B6", "#16A085", "#117A65", "#B2BABB", "#7F8C8D", "#616A6B"]; | ||
|
|
||
| function drawAreaStack(id, labels, values, minX, maxX, minY, maxY) { | ||
| d3.select(d3.select(id).node().parentNode) | ||
| .style("padding", "8px 0 8px 8px") | ||
| .style("border-right", "0px solid white"); | ||
|
|
||
| // Setup svg using Bostock's margin convention | ||
| var margin = {top: 20, right: 40, bottom: 30, left: maxMarginLeftForTimeline}; | ||
| var width = 850 - margin.left - margin.right; | ||
| var height = 300 - margin.top - margin.bottom; | ||
|
|
||
| var svg = d3.select(id) | ||
| .append("svg") | ||
| .attr("width", width + margin.left + margin.right) | ||
| .attr("height", height + margin.top + margin.bottom) | ||
| .append("g") | ||
| .attr("transform", "translate(" + margin.left + "," + margin.top + ")"); | ||
|
|
||
| var data = values; | ||
|
|
||
| var parse = d3.time.format("%H:%M:%S.%L").parse; | ||
|
|
||
| // Transpose the data into layers | ||
| var dataset = d3.layout.stack()(labels.map(function(fruit) { | ||
| return data.map(function(d) { | ||
| return {_x: d.x, x: parse(d.x), y: +d[fruit]}; | ||
| }); | ||
| })); | ||
|
|
||
|
|
||
| // Set x, y and colors | ||
| var x = d3.scale.ordinal() | ||
| .domain(dataset[0].map(function(d) { return d.x; })) | ||
| .rangeRoundBands([10, width-10], 0.02); | ||
|
|
||
| var y = d3.scale.linear() | ||
| .domain([0, d3.max(dataset, function(d) { return d3.max(d, function(d) { return d.y0 + d.y; }); })]) | ||
| .range([height, 0]); | ||
|
|
||
| var colors = colorPool.slice(0, labels.length) | ||
|
|
||
| // Define and draw axes | ||
| var yAxis = d3.svg.axis() | ||
| .scale(y) | ||
| .orient("left") | ||
| .ticks(7) | ||
| .tickFormat( function(d) { return d } ); | ||
|
|
||
| var xAxis = d3.svg.axis() | ||
| .scale(x) | ||
| .orient("bottom") | ||
| .tickFormat(d3.time.format("%H:%M:%S.%L")); | ||
|
|
||
| // Only show the first and last time in the graph | ||
| var xline = [] | ||
| xline.push(x.domain()[0]) | ||
| xline.push(x.domain()[x.domain().length - 1]) | ||
| xAxis.tickValues(xline); | ||
|
|
||
| svg.append("g") | ||
| .attr("class", "y axis") | ||
| .call(yAxis) | ||
| .append("text") | ||
| .attr("transform", "translate(0," + unitLabelYOffset + ")") | ||
| .text("ms"); | ||
|
|
||
| svg.append("g") | ||
| .attr("class", "x axis") | ||
| .attr("transform", "translate(0," + height + ")") | ||
| .call(xAxis); | ||
|
|
||
| // Create groups for each series, rects for each segment | ||
| var groups = svg.selectAll("g.cost") | ||
| .data(dataset) | ||
| .enter().append("g") | ||
| .attr("class", "cost") | ||
| .style("fill", function(d, i) { return colors[i]; }); | ||
|
|
||
| var rect = groups.selectAll("rect") | ||
| .data(function(d) { return d; }) | ||
| .enter() | ||
| .append("rect") | ||
| .attr("x", function(d) { return x(d.x); }) | ||
| .attr("y", function(d) { return y(d.y0 + d.y); }) | ||
| .attr("height", function(d) { return y(d.y0) - y(d.y0 + d.y); }) | ||
uncleGen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| .attr("width", x.rangeBand()) | ||
| .on('mouseover', function(d) { | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's more user-friendly to show the labels without mouse over.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I met some front end format which was difficult to adjust. :( |
||
| var tip = ''; | ||
| var idx = 0; | ||
| var _values = timeToValues[d._x] | ||
| _values.forEach(function (k) { | ||
| tip += labels[idx] + ': ' + k + ' '; | ||
| idx += 1; | ||
| }); | ||
| tip += " at " + d._x | ||
| showBootstrapTooltip(d3.select(this).node(), tip); | ||
| }) | ||
| .on('mouseout', function() { | ||
| hideBootstrapTooltip(d3.select(this).node()); | ||
| }) | ||
| .on("mousemove", function(d) { | ||
| var xPosition = d3.mouse(this)[0] - 15; | ||
| var yPosition = d3.mouse(this)[1] - 25; | ||
| tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")"); | ||
| tooltip.select("text").text(d.y); | ||
| }); | ||
|
|
||
|
|
||
| // Draw legend | ||
| var legend = svg.selectAll(".legend") | ||
| .data(colors) | ||
| .enter().append("g") | ||
| .attr("class", "legend") | ||
| .attr("transform", function(d, i) { return "translate(30," + i * 19 + ")"; }); | ||
|
|
||
| legend.append("rect") | ||
| .attr("x", width - 20) | ||
| .attr("width", 18) | ||
| .attr("height", 18) | ||
| .style("fill", function(d, i) {return colors.slice().reverse()[i];}) | ||
| .on('mouseover', function(d, i) { | ||
| var len = labels.length | ||
| showBootstrapTooltip(d3.select(this).node(), labels[len - 1 - i]); | ||
| }) | ||
| .on('mouseout', function() { | ||
| hideBootstrapTooltip(d3.select(this).node()); | ||
| }) | ||
| .on("mousemove", function(d) { | ||
| var xPosition = d3.mouse(this)[0] - 15; | ||
| var yPosition = d3.mouse(this)[1] - 25; | ||
| tooltip.attr("transform", "translate(" + xPosition + "," + yPosition + ")"); | ||
| tooltip.select("text").text(d.y); | ||
| }); | ||
|
|
||
| // Prep the tooltip bits, initial display is hidden | ||
| var tooltip = svg.append("g") | ||
| .attr("class", "tooltip") | ||
| .style("display", "none"); | ||
|
|
||
| tooltip.append("rect") | ||
| .attr("width", 30) | ||
| .attr("height", 20) | ||
| .attr("fill", "white") | ||
| .style("opacity", 0.5); | ||
|
|
||
| tooltip.append("text") | ||
| .attr("x", 15) | ||
| .attr("dy", "1.2em") | ||
| .style("text-anchor", "middle") | ||
| .attr("font-size", "12px") | ||
| .attr("font-weight", "bold"); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| /* | ||
| * 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. | ||
| */ | ||
|
|
||
| package org.apache.spark.ui | ||
|
|
||
| import java.{util => ju} | ||
| import java.lang.{Long => JLong} | ||
|
|
||
| import scala.collection.JavaConverters._ | ||
| import scala.collection.mutable.ArrayBuffer | ||
| import scala.xml.{Node, Unparsed} | ||
|
|
||
| /** | ||
| * A helper class to generate JavaScript and HTML for both timeline and histogram graphs. | ||
| * | ||
| * @param timelineDivId the timeline `id` used in the html `div` tag | ||
| * @param histogramDivId the timeline `id` used in the html `div` tag | ||
| * @param data the data for the graph | ||
| * @param minX the min value of X axis | ||
| * @param maxX the max value of X axis | ||
| * @param minY the min value of Y axis | ||
| * @param maxY the max value of Y axis | ||
| * @param unitY the unit of Y axis | ||
| * @param batchInterval if `batchInterval` is not None, we will draw a line for `batchInterval` in | ||
| * the graph | ||
| */ | ||
| private[spark] class GraphUIData( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: This class is moved from |
||
| timelineDivId: String, | ||
| histogramDivId: String, | ||
| data: Seq[(Long, Double)], | ||
| minX: Long, | ||
| maxX: Long, | ||
| minY: Double, | ||
| maxY: Double, | ||
| unitY: String, | ||
| batchInterval: Option[Double] = None) { | ||
|
|
||
| private var dataJavaScriptName: String = _ | ||
|
|
||
| def generateDataJs(jsCollector: JsCollector): Unit = { | ||
| val jsForData = data.map { case (x, y) => | ||
| s"""{"x": $x, "y": $y}""" | ||
| }.mkString("[", ",", "]") | ||
| dataJavaScriptName = jsCollector.nextVariableName | ||
| jsCollector.addPreparedStatement(s"var $dataJavaScriptName = $jsForData;") | ||
| } | ||
|
|
||
| def generateTimelineHtml(jsCollector: JsCollector): Seq[Node] = { | ||
| jsCollector.addPreparedStatement(s"registerTimeline($minY, $maxY);") | ||
| if (batchInterval.isDefined) { | ||
| jsCollector.addStatement( | ||
| "drawTimeline(" + | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I do not meet this error, could you please test again base on latest commit?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @uncleGen I hit the error as well.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created a PR #27883 for this. |
||
| s"'#$timelineDivId', $dataJavaScriptName, $minX, $maxX, $minY, $maxY, '$unitY'," + | ||
| s" ${batchInterval.get}" + | ||
| ");") | ||
| } else { | ||
| jsCollector.addStatement( | ||
| s"drawTimeline('#$timelineDivId', $dataJavaScriptName, $minX, $maxX, $minY, $maxY," + | ||
| s" '$unitY');") | ||
| } | ||
| <div id={timelineDivId}></div> | ||
| } | ||
|
|
||
| def generateHistogramHtml(jsCollector: JsCollector): Seq[Node] = { | ||
| val histogramData = s"$dataJavaScriptName.map(function(d) { return d.y; })" | ||
| jsCollector.addPreparedStatement(s"registerHistogram($histogramData, $minY, $maxY);") | ||
| if (batchInterval.isDefined) { | ||
| jsCollector.addStatement( | ||
| "drawHistogram(" + | ||
| s"'#$histogramDivId', $histogramData, $minY, $maxY, '$unitY', ${batchInterval.get}" + | ||
| ");") | ||
| } else { | ||
| jsCollector.addStatement( | ||
| s"drawHistogram('#$histogramDivId', $histogramData, $minY, $maxY, '$unitY');") | ||
| } | ||
| <div id={histogramDivId}></div> | ||
| } | ||
|
|
||
| def generateAreaStackHtmlWithData( | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. FYI: newly added |
||
| jsCollector: JsCollector, | ||
| values: Array[(Long, ju.Map[String, JLong])]): Seq[Node] = { | ||
| val operationLabels = values.flatMap(_._2.keySet().asScala).toSet | ||
| val durationDataPadding = UIUtils.durationDataPadding(values) | ||
| val jsForData = durationDataPadding.map { case (x, y) => | ||
| val s = y.toSeq.sortBy(_._1).map(e => s""""${e._1}": "${e._2}"""").mkString(",") | ||
| s"""{x: "${UIUtils.formatBatchTime(x, 1, showYYYYMMSS = false)}", $s}""" | ||
| }.mkString("[", ",", "]") | ||
| val jsForLabels = operationLabels.toSeq.sorted.mkString("[\"", "\",\"", "\"]") | ||
|
|
||
| val (maxX, minX, maxY, minY) = if (values != null && values.length > 0) { | ||
| val xValues = values.map(_._1.toLong) | ||
| val yValues = values.map(_._2.asScala.toSeq.map(_._2.toLong).sum) | ||
| (xValues.max, xValues.min, yValues.max, yValues.min) | ||
| } else { | ||
| (0L, 0L, 0L, 0L) | ||
| } | ||
|
|
||
| dataJavaScriptName = jsCollector.nextVariableName | ||
| jsCollector.addPreparedStatement(s"var $dataJavaScriptName = $jsForData;") | ||
| val labels = jsCollector.nextVariableName | ||
| jsCollector.addPreparedStatement(s"var $labels = $jsForLabels;") | ||
| jsCollector.addStatement( | ||
| s"drawAreaStack('#$timelineDivId', $labels, $dataJavaScriptName, $minX, $maxX, $minY, $maxY)") | ||
| <div id={timelineDivId}></div> | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * A helper class that allows the user to add JavaScript statements which will be executed when the | ||
| * DOM has finished loading. | ||
| */ | ||
| private[spark] class JsCollector { | ||
uncleGen marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| private var variableId = 0 | ||
|
|
||
| /** | ||
| * Return the next unused JavaScript variable name | ||
| */ | ||
| def nextVariableName: String = { | ||
| variableId += 1 | ||
| "v" + variableId | ||
| } | ||
|
|
||
| /** | ||
| * JavaScript statements that will execute before `statements` | ||
| */ | ||
| private val preparedStatements = ArrayBuffer[String]() | ||
|
|
||
| /** | ||
| * JavaScript statements that will execute after `preparedStatements` | ||
| */ | ||
| private val statements = ArrayBuffer[String]() | ||
|
|
||
| def addPreparedStatement(js: String): Unit = { | ||
| preparedStatements += js | ||
| } | ||
|
|
||
| def addStatement(js: String): Unit = { | ||
| statements += js | ||
| } | ||
|
|
||
| /** | ||
| * Generate a html snippet that will execute all scripts when the DOM has finished loading. | ||
| */ | ||
| def toHtml: Seq[Node] = { | ||
| val js = | ||
| s""" | ||
| |$$(document).ready(function() { | ||
| | ${preparedStatements.mkString("\n")} | ||
| | ${statements.mkString("\n")} | ||
| |});""".stripMargin | ||
|
|
||
| <script>{Unparsed(js)}</script> | ||
| } | ||
| } | ||

There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those colors have very similar hue so it's difficult to distinguish one from another one.
I think, previous choice of colors are much better.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For color and display stuff maybe we could keep the current approach, and wait for more advice from end-users. All the changes could be done in follow-ups.