You are viewing a plain text version of this content. The canonical link for it is here.
Posted to notifications@superset.apache.org by GitBox <gi...@apache.org> on 2018/07/24 17:41:20 UTC

[GitHub] williaster closed pull request #4313: [big number] improve visual aesthetics of big number visualization

williaster closed pull request #4313: [big number] improve visual aesthetics of big number visualization
URL: https://github.com/apache/incubator-superset/pull/4313
 
 
   

This is a PR merged from a forked repository.
As GitHub hides the original diff on merge, it is displayed below for
the sake of provenance:

As this is a foreign pull request (from a fork), the diff is supplied
below (as it won't show otherwise due to GitHub magic):

diff --git a/superset/assets/package.json b/superset/assets/package.json
index c944ad2fa0..b7653090f5 100644
--- a/superset/assets/package.json
+++ b/superset/assets/package.json
@@ -42,6 +42,7 @@
   "dependencies": {
     "@data-ui/event-flow": "^0.0.8",
     "@data-ui/sparkline": "^0.0.49",
+    "@data-ui/xy-chart": "^0.0.50",
     "babel-register": "^6.24.1",
     "bootstrap": "^3.3.6",
     "brace": "^0.10.0",
diff --git a/superset/assets/visualizations/BigNumber.jsx b/superset/assets/visualizations/BigNumber.jsx
new file mode 100644
index 0000000000..f470d4905f
--- /dev/null
+++ b/superset/assets/visualizations/BigNumber.jsx
@@ -0,0 +1,188 @@
+import React from 'react';
+import ReactDOM from 'react-dom';
+import { XYChart, AreaSeries, PointSeries, CrossHair, LinearGradient } from '@data-ui/xy-chart';
+
+import { brandColor } from '../javascripts/modules/colors';
+import { d3FormatPreset, d3TimeFormatPreset } from '../javascripts/modules/utils';
+import { getTextWidth } from '../javascripts/modules/visUtils';
+
+const fontFamily = '-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif';
+
+const CONTAINER_PADDING = 16;
+
+const CONTAINER_STYLES = {
+  fontFamily,
+  padding: CONTAINER_PADDING,
+  overflow: 'hidden',
+  display: 'flex',
+  flexDirection: 'column',
+  justifyContent: 'center',
+};
+
+const BIG_NUMBER_STYLES = {
+  lineHeight: '1em',
+  paddingTop: '0.37em',
+  paddingBottom: '0.37em',
+  fontWeight: 600,
+};
+
+const SUBHEADER_STYLES = {
+  fontWeight: 200,
+  paddingTop: '0.5em',
+  paddingBottom: '0.5em',
+  lineHeight: '1em',
+};
+
+const CHART_MARGIN = {
+  top: 4,
+  right: 4,
+  bottom: 4,
+  left: 4,
+};
+
+const TOOLTIP_STYLES = {
+  padding: '4px 8px',
+};
+
+const NUMBER_SIZE_SCALAR = 0.275;
+const CHART_SIZE_SCALAR = 0.2;
+const TEXT_SIZE_SCALAR = 0.2;
+
+// Returns the maximum font size (<= idealFontSize) that will fit within the availableWidth
+function getMaxFontSize({ text, availableWidth, idealFontSize, fontWeight = 'normal' }) {
+  let fontSize = idealFontSize;
+  let textWidth = getTextWidth(text, `${fontWeight} ${fontSize}px ${fontFamily}`);
+  while (textWidth > availableWidth) {
+    fontSize -= 2;
+    textWidth = getTextWidth(text, `${fontWeight} ${fontSize}px ${fontFamily}`);
+  }
+  return fontSize;
+}
+
+function renderTooltipFactory({ formatDate, formatValue }) {
+  return function renderTooltip({ datum }) { // eslint-disable-line
+    const { x: rawDate, y: rawValue } = datum;
+    const formattedDate = formatDate(rawDate);
+    const value = formatValue(rawValue);
+
+    return (
+      <div style={TOOLTIP_STYLES}>
+        {formattedDate}
+        <br />
+        <strong>{value}</strong>
+      </div>
+    );
+  };
+}
+function bigNumberVis(slice, payload) {
+  const { formData, containerId } = slice;
+  const json = payload.data;
+  const { data, subheader, compare_lag: compareLag, compare_suffix: compareSuffix } = json;
+  const showTrendline = formData.viz_type === 'big_number';
+  const formatValue = d3FormatPreset(formData.y_axis_format);
+  const formatPercentChange = d3.format('+.1%');
+  const formatDate = d3TimeFormatPreset('smart_date');
+  const renderTooltip = renderTooltipFactory({ formatDate, formatValue });
+  const gradientId = `big_number_${containerId}`;
+  const totalWidth = slice.width();
+  const totalHeight = slice.height();
+  const availableWidth = totalWidth - 2 * CONTAINER_PADDING;
+
+  const bigNumber = showTrendline ? data[data.length - 1][1] : data[0][0];
+  let percentChange = null;
+
+  if (showTrendline && compareLag > 0) {
+    const compareIndex = data.length - (compareLag + 1);
+    if (compareIndex >= 0) {
+      const compareValue = data[compareIndex][1];
+      percentChange = compareValue === 0
+        ? 0 : (bigNumber - compareValue) / Math.abs(compareValue);
+    }
+  }
+
+  const formattedBigNumber = formatValue(bigNumber);
+  const formattedData = showTrendline ? data.map(d => ({ x: d[0], y: d[1] })) : null;
+  const formattedSubheader = percentChange === null ? subheader : (
+    `${formatPercentChange(percentChange)} ${(compareSuffix || '').trim()}`
+  );
+
+  const bigNumberFontSize = getMaxFontSize({
+    text: formattedBigNumber,
+    availableWidth,
+    idealFontSize: totalHeight * NUMBER_SIZE_SCALAR,
+    fontWeight: BIG_NUMBER_STYLES.fontWeight,
+  });
+
+  const subheaderFontSize = formattedSubheader ? getMaxFontSize({
+    text: formattedSubheader,
+    availableWidth,
+    idealFontSize: totalHeight * TEXT_SIZE_SCALAR,
+    fontWeight: SUBHEADER_STYLES.fontWeight,
+  }) : null;
+
+  const Visualization = (
+    <div style={{ height: totalHeight, ...CONTAINER_STYLES }}>
+      <div
+        style={{
+          fontSize: bigNumberFontSize,
+          ...BIG_NUMBER_STYLES,
+        }}
+      >
+        {formattedBigNumber}
+      </div>
+
+      {showTrendline &&
+        <XYChart
+          ariaLabel={`Big number visualization ${subheader}`}
+          xScale={{ type: 'timeUtc' }}
+          yScale={{ type: 'linear' }}
+          width={availableWidth}
+          height={totalHeight * CHART_SIZE_SCALAR}
+          margin={CHART_MARGIN}
+          renderTooltip={renderTooltip}
+          snapTooltipToDataX
+        >
+          <LinearGradient
+            id={gradientId}
+            from={brandColor}
+            to="#fff"
+          />
+          <AreaSeries
+            data={formattedData}
+            fill={`url(#${gradientId})`}
+            stroke={brandColor}
+          />
+          <PointSeries
+            fill={brandColor}
+            stroke="#fff"
+            data={[formattedData[formattedData.length - 1]]}
+          />
+          <CrossHair
+            stroke={brandColor}
+            circleFill={brandColor}
+            circleStroke="#fff"
+            showHorizontalLine={false}
+            fullHeight
+            strokeDasharray="5,2"
+          />
+        </XYChart>}
+
+      {formattedSubheader &&
+        <div
+          style={{
+            fontSize: subheaderFontSize,
+            ...SUBHEADER_STYLES,
+          }}
+        >
+          {formattedSubheader}
+        </div>}
+    </div>
+  );
+
+  ReactDOM.render(
+    Visualization,
+    document.getElementById(containerId),
+  );
+}
+
+module.exports = bigNumberVis;
diff --git a/superset/assets/visualizations/big_number.js b/superset/assets/visualizations/big_number.js
deleted file mode 100644
index f0c3950a25..0000000000
--- a/superset/assets/visualizations/big_number.js
+++ /dev/null
@@ -1,229 +0,0 @@
-import d3 from 'd3';
-import d3tip from 'd3-tip';
-import { d3FormatPreset, d3TimeFormatPreset } from '../javascripts/modules/utils';
-
-import './big_number.css';
-import '../stylesheets/d3tip.css';
-
-function bigNumberVis(slice, payload) {
-  const div = d3.select(slice.selector);
-  // Define the percentage bounds that define color from red to green
-  div.html(''); // reset
-  const fd = slice.formData;
-  const json = payload.data;
-
-  const f = d3FormatPreset(fd.y_axis_format);
-  const fp = d3.format('+.1%');
-  const formatDate = d3TimeFormatPreset('smart_date');
-  const width = slice.width();
-  const height = slice.height();
-  const svg = div.append('svg');
-  svg.attr('width', width);
-  svg.attr('height', height);
-  const data = json.data;
-  let vCompare;
-  let v;
-  if (fd.viz_type === 'big_number') {
-    v = data[data.length - 1][1];
-  } else {
-    v = data[0][0];
-  }
-  if (json.compare_lag > 0) {
-    const pos = data.length - (json.compare_lag + 1);
-    if (pos >= 0) {
-      const vAnchor = data[pos][1];
-      if (vAnchor !== 0) {
-        vCompare = (v - vAnchor) / Math.abs(vAnchor);
-      } else {
-        vCompare = 0;
-      }
-    }
-  }
-  const dateExt = d3.extent(data, d => d[0]);
-  const valueExt = d3.extent(data, d => d[1]);
-
-  const vMargin = 20;
-  const hMargin = 10;
-  const scaleX = d3.time.scale.utc().domain(dateExt).range([hMargin, width - hMargin]);
-  const scaleY = d3.scale.linear().domain(valueExt).range([height - (vMargin), vMargin]);
-  const colorRange = [d3.hsl(0, 1, 0.3), d3.hsl(120, 1, 0.3)];
-  const scaleColor = d3.scale
-  .linear().domain([-1, 1])
-  .interpolate(d3.interpolateHsl)
-  .range(colorRange)
-  .clamp(true);
-  const line = d3.svg.line()
-  .x(function (d) {
-    return scaleX(d[0]);
-  })
-  .y(function (d) {
-    return scaleY(d[1]);
-  })
-  .interpolate('cardinal');
-
-  let y = height / 2;
-  let g = svg.append('g');
-  // Printing big number
-  g.append('g').attr('class', 'digits')
-  .attr('opacity', 1)
-  .append('text')
-  .attr('x', width / 2)
-  .attr('y', y)
-  .attr('class', 'big')
-  .attr('alignment-baseline', 'middle')
-  .attr('id', 'bigNumber')
-  .style('font-weight', 'bold')
-  .style('cursor', 'pointer')
-  .text(f(v))
-  .style('font-size', d3.min([height, width]) / 3.5)
-  .style('text-anchor', 'middle')
-  .attr('fill', 'black');
-
-  // Printing big number subheader text
-  if (json.subheader !== null) {
-    g.append('text')
-    .attr('x', width / 2)
-    .attr('y', (height / 16) * 12)
-    .text(json.subheader)
-    .attr('id', 'subheader_text')
-    .style('font-size', d3.min([height, width]) / 8)
-    .style('text-anchor', 'middle');
-  }
-
-  if (fd.viz_type === 'big_number') {
-    // Drawing trend line
-
-    g.append('path')
-    .attr('d', function () {
-      return line(data);
-    })
-    .attr('stroke-width', 5)
-    .attr('opacity', 0.5)
-    .attr('fill', 'none')
-    .attr('stroke-linecap', 'round')
-    .attr('stroke', 'grey');
-
-    g = svg.append('g')
-    .attr('class', 'digits')
-    .attr('opacity', 1);
-
-    if (vCompare !== null) {
-      y = (height / 8) * 3;
-    }
-
-    const c = scaleColor(vCompare);
-
-    // Printing compare %
-    if (vCompare) {
-      g.append('text')
-      .attr('x', width / 2)
-      .attr('y', (height / 16) * 12)
-      .text(fp(vCompare) + json.compare_suffix)
-      .style('font-size', d3.min([height, width]) / 8)
-      .style('text-anchor', 'middle')
-      .attr('fill', c)
-      .attr('stroke', c);
-    }
-
-    const gAxis = svg.append('g').attr('class', 'axis').attr('opacity', 0);
-    g = gAxis.append('g');
-    const xAxis = d3.svg.axis()
-    .scale(scaleX)
-    .orient('bottom')
-    .ticks(Math.round(2 + (width / 150)))
-    .tickFormat(formatDate);
-    g.call(xAxis);
-    g.attr('transform', 'translate(0,' + (height - vMargin) + ')');
-
-    g = gAxis.append('g').attr('transform', 'translate(' + (width - hMargin) + ',0)');
-    const yAxis = d3.svg.axis()
-    .scale(scaleY)
-    .orient('left')
-    .tickFormat(f)
-    .tickValues(valueExt);
-
-    g.call(yAxis);
-    g.selectAll('text')
-    .style('text-anchor', 'end')
-    .attr('y', '-7')
-    .attr('x', '-4');
-
-    g.selectAll('text')
-    .style('font-size', '10px');
-
-    const renderTooltip = (d) => {
-      const date = formatDate(d[0]);
-      const value = f(d[1]);
-      return `
-        <div>
-          <span style="margin-right: 10px;">${date}: </span>
-          <strong>${value}</strong>
-        </div>
-      `;
-    };
-
-    const tip = d3tip()
-      .attr('class', 'd3-tip')
-      .direction('n')
-      .offset([-5, 0])
-      .html(renderTooltip);
-    svg.call(tip);
-
-    // Add the scatterplot and trigger the mouse events for the tooltips
-    svg
-      .selectAll('dot')
-      .data(data)
-      .enter()
-      .append('circle')
-      .attr('r', 3)
-      .attr('stroke-width', 15)
-      .attr('stroke', 'transparent')
-      .attr('stroke-location', 'outside')
-      .attr('cx', d => scaleX(d[0]))
-      .attr('cy', d => scaleY(d[1]))
-      .attr('fill-opacity', 0)
-      .on('mouseover', function (d) {
-        d3.select(this).attr('fill-opacity', 1);
-        tip.show(d);
-      })
-      .on('mouseout', function (d) {
-        d3.select(this).attr('fill-opacity', 0);
-        tip.hide(d);
-      });
-
-    div.on('mouseover', function () {
-      const el = d3.select(this);
-      el.selectAll('path')
-      .transition()
-      .duration(500)
-      .attr('opacity', 1)
-      .style('stroke-width', '2px');
-      el.selectAll('g.digits')
-      .transition()
-      .duration(500)
-      .attr('opacity', 0.1);
-      el.selectAll('g.axis')
-      .transition()
-      .duration(500)
-      .attr('opacity', 1);
-    })
-    .on('mouseout', function () {
-      const el = d3.select(this);
-      el.select('path')
-      .transition()
-      .duration(500)
-      .attr('opacity', 0.5)
-      .style('stroke-width', '5px');
-      el.selectAll('g.digits')
-      .transition()
-      .duration(500)
-      .attr('opacity', 1);
-      el.selectAll('g.axis')
-      .transition()
-      .duration(500)
-      .attr('opacity', 0);
-    });
-  }
-}
-
-module.exports = bigNumberVis;
diff --git a/superset/assets/visualizations/main.js b/superset/assets/visualizations/main.js
index 93c12e8617..10d28b38f2 100644
--- a/superset/assets/visualizations/main.js
+++ b/superset/assets/visualizations/main.js
@@ -54,8 +54,8 @@ export const VIZ_TYPES = {
 const vizMap = {
   [VIZ_TYPES.area]: require('./nvd3_vis.js'),
   [VIZ_TYPES.bar]: require('./nvd3_vis.js'),
-  [VIZ_TYPES.big_number]: require('./big_number.js'),
-  [VIZ_TYPES.big_number_total]: require('./big_number.js'),
+  [VIZ_TYPES.big_number]: require('./BigNumber.jsx'),
+  [VIZ_TYPES.big_number_total]: require('./BigNumber.jsx'),
   [VIZ_TYPES.box_plot]: require('./nvd3_vis.js'),
   [VIZ_TYPES.bubble]: require('./nvd3_vis.js'),
   [VIZ_TYPES.bullet]: require('./nvd3_vis.js'),


 

----------------------------------------------------------------
This is an automated message from the Apache Git Service.
To respond to the message, please log on GitHub and use the
URL above to go to the specific comment.
 
For queries about this service, please contact Infrastructure at:
users@infra.apache.org


With regards,
Apache Git Services

---------------------------------------------------------------------
To unsubscribe, e-mail: notifications-unsubscribe@superset.apache.org
For additional commands, e-mail: notifications-help@superset.apache.org