diff --git a/superset/assets/package.json b/superset/assets/package.json index 8c15bfaeecf90..b5ce7d848d4ae 100644 --- a/superset/assets/package.json +++ b/superset/assets/package.json @@ -66,9 +66,13 @@ "d3-array": "^1.2.4", "d3-cloud": "^1.2.1", "d3-color": "^1.2.0", + "d3-format": "^1.3.2", "d3-hierarchy": "^1.1.5", "d3-sankey": "^0.4.2", + "d3-scale": "^2.1.2", + "d3-selection": "^1.3.2", "d3-svg-legend": "^1.x", + "d3-time-format": "^2.1.3", "d3-tip": "^0.9.1", "datamaps": "^0.5.8", "datatables.net-bs": "^1.10.15", diff --git a/superset/assets/src/modules/geo.js b/superset/assets/src/modules/geo.js index e689a4168235a..2fc2744ae49de 100644 --- a/superset/assets/src/modules/geo.js +++ b/superset/assets/src/modules/geo.js @@ -1,3 +1,5 @@ +import { round as d3Round } from 'd3-format'; + export const defaultViewport = { longitude: 6.85236157047845, latitude: 31.222656842808707, @@ -7,6 +9,7 @@ export const defaultViewport = { }; const METER_TO_MILE = 1609.34; + export function unitToRadius(unit, num) { if (unit === 'square_m') { return Math.sqrt(num / Math.PI); @@ -23,3 +26,14 @@ export function unitToRadius(unit, num) { } return null; } + +export const EARTH_CIRCUMFERENCE_KM = 40075.16; +export const MILES_PER_KM = 1.60934; + +export function kmToPixels(kilometers, latitude, zoomLevel) { + // Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels + const latitudeRad = latitude * (Math.PI / 180); + // Seems like the zoomLevel is off by one + const kmPerPixel = (EARTH_CIRCUMFERENCE_KM * Math.cos(latitudeRad)) / Math.pow(2, zoomLevel + 9); + return d3Round(kilometers / kmPerPixel, 2); +} diff --git a/superset/assets/src/modules/utils.js b/superset/assets/src/modules/utils.js index bc22c5a06b043..1bd82e21ec082 100644 --- a/superset/assets/src/modules/utils.js +++ b/superset/assets/src/modules/utils.js @@ -1,9 +1,11 @@ /* eslint camelcase: 0 */ import $ from 'jquery'; -import d3 from 'd3'; +import { format as d3Format } from 'd3-format'; +import { d3Select } from 'd3-selection'; +import { timeFormat as d3TimeFormat } from 'd3-time-format'; import { formatDate, UTC } from './dates'; -const siFormatter = d3.format('.3s'); +const siFormatter = d3Format('.3s'); export function defaultNumberFormatter(n) { let si = siFormatter(n); @@ -15,27 +17,43 @@ export function defaultNumberFormatter(n) { } export function d3FormatPreset(format) { - // like d3.format, but with support for presets like 'smart_date' + // like d3Format, but with support for presets like 'smart_date' if (format === 'smart_date') { return formatDate; } if (format) { - return d3.format(format); + return d3Format(format); } return defaultNumberFormatter; } + export const d3TimeFormatPreset = function (format) { const effFormat = format || 'smart_date'; if (effFormat === 'smart_date') { return formatDate; } - const f = d3.time.format(effFormat); + const f = d3TimeFormat(effFormat); return function (dttm) { const d = UTC(new Date(dttm)); return f(d); }; }; +const formatters = {}; + +export function d3format(format, number) { + format = format || '.3s'; + // Formats a number and memoizes formatters to be reused + if (!(format in formatters)) { + formatters[format] = d3Format(format); + } + try { + return formatters[format](number); + } catch (e) { + return 'ERR'; + } +} + /* Utility function that takes a d3 svg:text selection and a max width, and splits the text's text across multiple tspan lines such that any given line does not exceed max width @@ -47,7 +65,7 @@ export function wrapSvgText(text, width, adjustedY) { const lineHeight = 1; // ems text.each(function () { - const d3Text = d3.select(this); + const d3Text = d3Select(this); const words = d3Text.text().split(/\s+/); let word; let line = []; @@ -118,20 +136,6 @@ export const fixDataTableBodyHeight = function ($tableDom, height) { $tableDom.find('.dataTables_scrollBody').css('max-height', height - headHeight - controlsHeight - paginationHeight); }; -export function d3format(format, number) { - const formatters = {}; - // Formats a number and memoizes formatters to be reused - format = format || '.3s'; - if (!(format in formatters)) { - formatters[format] = d3.format(format); - } - try { - return formatters[format](number); - } catch (e) { - return 'ERR'; - } -} - export function formatSelectOptionsForRange(start, end) { // outputs array of arrays // formatSelectOptionsForRange(1, 5) diff --git a/superset/assets/src/utils/common.js b/superset/assets/src/utils/common.js index 282518f042669..e044df1eeeb37 100644 --- a/superset/assets/src/utils/common.js +++ b/superset/assets/src/utils/common.js @@ -1,24 +1,13 @@ -import d3 from 'd3'; import { SupersetClient } from '@superset-ui/connection'; import getClientErrorObject from './getClientErrorObject'; -export const EARTH_CIRCUMFERENCE_KM = 40075.16; export const LUMINANCE_RED_WEIGHT = 0.2126; export const LUMINANCE_GREEN_WEIGHT = 0.7152; export const LUMINANCE_BLUE_WEIGHT = 0.0722; -export const MILES_PER_KM = 1.60934; // Regexp for the label added to time shifted series (1 hour offset, 2 days offset, etc.) export const TIME_SHIFT_PATTERN = /\d+ \w+ offset/; -export function kmToPixels(kilometers, latitude, zoomLevel) { - // Algorithm from: http://wiki.openstreetmap.org/wiki/Zoom_levels - const latitudeRad = latitude * (Math.PI / 180); - // Seems like the zoomLevel is off by one - const kmPerPixel = (EARTH_CIRCUMFERENCE_KM * Math.cos(latitudeRad)) / Math.pow(2, zoomLevel + 9); - return d3.round(kilometers / kmPerPixel, 2); -} - export function rgbLuminance(r, g, b) { // Formula: https://en.wikipedia.org/wiki/Relative_luminance return LUMINANCE_RED_WEIGHT * r + LUMINANCE_GREEN_WEIGHT * g + LUMINANCE_BLUE_WEIGHT * b; diff --git a/superset/assets/src/visualizations/BigNumber/transformProps.js b/superset/assets/src/visualizations/BigNumber/transformProps.js index 41a9df5a37ef2..a046da35cb887 100644 --- a/superset/assets/src/visualizations/BigNumber/transformProps.js +++ b/superset/assets/src/visualizations/BigNumber/transformProps.js @@ -1,5 +1,5 @@ import * as color from 'd3-color'; -import d3 from 'd3'; +import { format as d3Format } from 'd3-format'; import { d3FormatPreset } from '../../modules/utils'; import { renderTooltipFactory } from './BigNumber'; @@ -43,7 +43,7 @@ export default function transformProps(chartProps) { const compareValue = sortedData[compareIndex][metricName]; percentChange = compareValue === 0 ? 0 : (bigNumber - compareValue) / Math.abs(compareValue); - const formatPercentChange = d3.format('+.1%'); + const formatPercentChange = d3Format('+.1%'); formattedSubheader = `${formatPercentChange(percentChange)} ${compareSuffix}`; } } diff --git a/superset/assets/src/visualizations/Horizon/HorizonChart.jsx b/superset/assets/src/visualizations/Horizon/HorizonChart.jsx index f9bb05a7ecc5c..655e89cd4e1a2 100644 --- a/superset/assets/src/visualizations/Horizon/HorizonChart.jsx +++ b/superset/assets/src/visualizations/Horizon/HorizonChart.jsx @@ -1,6 +1,6 @@ import React from 'react'; import PropTypes from 'prop-types'; -import d3 from 'd3'; +import { extent as d3Extent } from 'd3-array'; import HorizonRow, { DEFAULT_COLORS } from './HorizonRow'; import './HorizonChart.css'; @@ -52,7 +52,7 @@ class HorizonChart extends React.PureComponent { (acc, current) => acc.concat(current.values), [], ); - yDomain = d3.extent(allValues, d => d.y); + yDomain = d3Extent(allValues, d => d.y); } return ( diff --git a/superset/assets/src/visualizations/Horizon/HorizonRow.jsx b/superset/assets/src/visualizations/Horizon/HorizonRow.jsx index fd96ad5f800f2..df696570e188e 100644 --- a/superset/assets/src/visualizations/Horizon/HorizonRow.jsx +++ b/superset/assets/src/visualizations/Horizon/HorizonRow.jsx @@ -1,6 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import d3 from 'd3'; +import { extent as d3Extent } from 'd3-array'; +import { scaleLinear } from 'd3-scale'; export const DEFAULT_COLORS = [ '#313695', @@ -91,8 +92,8 @@ class HorizonRow extends React.PureComponent { } // Create y-scale - const [min, max] = yDomain || d3.extent(data, d => d.y); - const y = d3.scale.linear() + const [min, max] = yDomain || d3Extent(data, d => d.y); + const y = scaleLinear() .domain([0, Math.max(-min, max)]) .range([0, height]); diff --git a/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx index e67302aaa9904..5a6ba27314833 100644 --- a/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx +++ b/superset/assets/src/visualizations/MapBox/ScatterPlotGlowOverlay.jsx @@ -1,13 +1,10 @@ -import d3 from 'd3'; import Immutable from 'immutable'; import React from 'react'; import PropTypes from 'prop-types'; import ViewportMercator from 'viewport-mercator-project'; -import { - kmToPixels, - rgbLuminance, - MILES_PER_KM, -} from '../../utils/common'; +import { round as d3Round } from 'd3-format'; +import { kmToPixels, MILES_PER_KM } from '../../modules/geo'; +import { rgbLuminance } from '../../utils/common'; const propTypes = { aggregation: PropTypes.string, @@ -131,7 +128,7 @@ class ScatterPlotGlowOverlay extends React.Component { if ((props.renderWhileDragging || !props.isDragging) && props.locations) { props.locations.forEach(function _forEach(location, i) { const pixel = mercator.project(props.lngLatAccessor(location)); - const pixelRounded = [d3.round(pixel[0], 1), d3.round(pixel[1], 1)]; + const pixelRounded = [d3Round(pixel[0], 1), d3Round(pixel[1], 1)]; if (pixelRounded[0] + radius >= 0 && pixelRounded[0] - radius < props.width @@ -140,8 +137,8 @@ class ScatterPlotGlowOverlay extends React.Component { ctx.beginPath(); if (location.get('properties').get('cluster')) { let clusterLabel = clusterLabelMap[i]; - const scaledRadius = d3.round(Math.pow(clusterLabel / maxLabel, 0.5) * radius, 1); - const fontHeight = d3.round(scaledRadius * 0.5, 1); + const scaledRadius = d3Round(Math.pow(clusterLabel / maxLabel, 0.5) * radius, 1); + const fontHeight = d3Round(scaledRadius * 0.5, 1); const gradient = ctx.createRadialGradient( pixelRounded[0], pixelRounded[1], scaledRadius, pixelRounded[0], pixelRounded[1], 0, @@ -177,17 +174,17 @@ class ScatterPlotGlowOverlay extends React.Component { if (radiusProperty !== null) { const pointLatitude = props.lngLatAccessor(location)[1]; if (props.pointRadiusUnit === 'Kilometers') { - pointLabel = d3.round(pointRadius, 2) + 'km'; + pointLabel = d3Round(pointRadius, 2) + 'km'; pointRadius = kmToPixels(pointRadius, pointLatitude, props.zoom); } else if (props.pointRadiusUnit === 'Miles') { - pointLabel = d3.round(pointRadius, 2) + 'mi'; + pointLabel = d3Round(pointRadius, 2) + 'mi'; pointRadius = kmToPixels(pointRadius * MILES_PER_KM, pointLatitude, props.zoom); } } if (pointMetric !== null) { pointLabel = Number.isFinite(parseFloat(pointMetric)) - ? d3.round(pointMetric, 2) + ? d3Round(pointMetric, 2) : pointMetric; } @@ -196,13 +193,13 @@ class ScatterPlotGlowOverlay extends React.Component { pointRadius = defaultRadius; } - ctx.arc(pixelRounded[0], pixelRounded[1], d3.round(pointRadius, 1), 0, Math.PI * 2); + ctx.arc(pixelRounded[0], pixelRounded[1], d3Round(pointRadius, 1), 0, Math.PI * 2); ctx.fillStyle = 'rgb(' + rgb[1] + ', ' + rgb[2] + ', ' + rgb[3] + ')'; ctx.fill(); if (pointLabel !== undefined) { this.drawText(ctx, pixelRounded, { - fontHeight: d3.round(pointRadius, 1), + fontHeight: d3Round(pointRadius, 1), label: pointLabel, radius: pointRadius, rgb, diff --git a/superset/assets/src/visualizations/Table/Table.js b/superset/assets/src/visualizations/Table/Table.js index 070a96c1ed40c..7056235e713b9 100644 --- a/superset/assets/src/visualizations/Table/Table.js +++ b/superset/assets/src/visualizations/Table/Table.js @@ -4,6 +4,7 @@ import PropTypes from 'prop-types'; import dt from 'datatables.net-bs'; import 'datatables.net-bs/css/dataTables.bootstrap.css'; import dompurify from 'dompurify'; +import { format as d3Format } from 'd3-format'; import { fixDataTableBodyHeight, d3TimeFormatPreset } from '../../modules/utils'; import './Table.css'; @@ -45,8 +46,8 @@ const propTypes = { ]), }; -const formatValue = d3.format('0,000'); -const formatPercent = d3.format('.3p'); +const formatValue = d3Format(',.0d'); +const formatPercent = d3Format('.3p'); function NOOP() {} function TableVis(element, props) { @@ -129,7 +130,7 @@ function TableVis(element, props) { html = `${dompurify.sanitize(val)}`; } if (isMetric) { - html = d3.format(format || '0.3s')(val); + html = d3Format(format || '0.3s')(val); } if (key[0] === '%') { html = formatPercent(val); diff --git a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx index 0eaafc7f6b943..38bf058b273f1 100644 --- a/superset/assets/src/visualizations/TimeTable/TimeTable.jsx +++ b/superset/assets/src/visualizations/TimeTable/TimeTable.jsx @@ -1,7 +1,7 @@ import React from 'react'; import PropTypes from 'prop-types'; -import d3 from 'd3'; import Mustache from 'mustache'; +import { scaleLinear } from 'd3-scale'; import { Table, Thead, Th, Tr, Td } from 'reactable'; import MetricOption from '../../components/MetricOption'; @@ -19,7 +19,7 @@ function colorFromBounds(value, bounds, colorBounds = ACCESSIBLE_COLOR_BOUNDS) { const [min, max] = bounds; const [minColor, maxColor] = colorBounds; if (min !== null && max !== null) { - const colorScale = d3.scale.linear() + const colorScale = scaleLinear() .domain([min, (max + min) / 2, max]) .range([minColor, 'grey', maxColor]); return colorScale(value); @@ -131,7 +131,7 @@ class TimeTable extends React.PureComponent { showYAxis={column.showYAxis} renderTooltip={({ index }) => (
- {d3format(column.d3Format, sparkData[index])} + {d3format(column.d3format, sparkData[index])}
{formatDate(entries[index].time)}
)} diff --git a/superset/assets/yarn.lock b/superset/assets/yarn.lock index b1ab09eabd765..cffbb366209b3 100644 --- a/superset/assets/yarn.lock +++ b/superset/assets/yarn.lock @@ -3943,7 +3943,7 @@ d3-ease@1: resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.5.tgz#8ce59276d81241b1b72042d6af2d40e76d936ffb" integrity sha512-Ct1O//ly5y5lFM9YTdu+ygq7LleSgSE4oj7vUt9tPLHUi8VCV7QoizGpdWRWAwCO9LdYzIrQDg97+hGVdsSGPQ== -d3-format@1, d3-format@^1.2.0: +d3-format@1, d3-format@^1.2.0, d3-format@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.3.2.tgz#6a96b5e31bcb98122a30863f7d92365c00603562" integrity sha512-Z18Dprj96ExragQ0DeGi+SYPQ7pPfRMtUXtsg/ChVIKNBCzjO8XYJvRTC1usblx52lqge56V5ect+frYTQc8WQ== @@ -4009,7 +4009,7 @@ d3-scale@^1.0.5, d3-scale@^1.0.6: d3-time "1" d3-time-format "2" -d3-scale@^2.0.0: +d3-scale@^2.0.0, d3-scale@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-2.1.2.tgz#4e932b7b60182aee9073ede8764c98423e5f9a94" integrity sha512-bESpd64ylaKzCDzvULcmHKZTlzA/6DGSVwx7QSDj/EnX9cpSevsdiwdHFYI9ouo9tNBbV3v5xztHS2uFeOzh8Q== @@ -4021,7 +4021,7 @@ d3-scale@^2.0.0: d3-time "1" d3-time-format "2" -d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.3.0: +d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.3.0, d3-selection@^1.3.2: version "1.3.2" resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.3.2.tgz#6e70a9df60801c8af28ac24d10072d82cbfdf652" integrity sha512-OoXdv1nZ7h2aKMVg3kaUFbLLK5jXUFAMLD/Tu5JA96mjf8f2a9ZUESGY+C36t8R1WFeWk/e55hy54Ml2I62CRQ== @@ -4038,7 +4038,7 @@ d3-svg-legend@^1.x: resolved "https://registry.yarnpkg.com/d3-svg-legend/-/d3-svg-legend-1.13.0.tgz#6217478c9add9d62cb333617e1961311a41a4db3" integrity sha1-YhdHjJrdnWLLMzYX4ZYTEaQaTbM= -d3-time-format@2: +d3-time-format@2, d3-time-format@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.3.tgz#ae06f8e0126a9d60d6364eac5b1533ae1bac826b" integrity sha512-6k0a2rZryzGm5Ihx+aFMuO1GgelgIz+7HhB4PH4OEndD5q2zGn1mDfRdNrulspOfR6JXkb2sThhDK41CSK85QA==