From 46b0ed53aee7934782059600755c391b55b62bf0 Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 26 Mar 2019 16:12:51 +0000 Subject: [PATCH 01/47] Put color styles in settings so that they can be scoped to a chart Allows us to put multiple charts on a single page, where theme settings apply to only some of them --- .../src/js/charts/ohlcCandle.js | 1 + .../src/js/plugin/template.js | 6 ++--- .../src/js/series/colorStyles.js | 27 ++++++++++++------- .../src/js/series/ohlcCandleSeries.js | 8 +++--- .../src/js/series/seriesColors.js | 25 +++++++++++++---- .../src/less/chart.less | 15 ++--------- 6 files changed, 49 insertions(+), 33 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js b/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js index 40816960f2..ab06b99df8 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js @@ -37,6 +37,7 @@ function ohlcCandle(seriesCanvas) { const upColor = colorScale() .domain(keys) + .settings(settings) .mapFunction(setOpacity(1))(); const legend = colorLegend() diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/template.js b/packages/perspective-viewer-d3fc/src/js/plugin/template.js index 441c0770c3..81145da9fc 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/template.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/template.js @@ -30,8 +30,6 @@ class D3FCChartElement extends HTMLElement { style.setAttribute("scope", "perspective-viewer"); style.textContent = perspectiveStyle; this.shadowRoot.host.getRootNode().appendChild(style); - - initialiseStyles(this._container); } render(chart, settings) { @@ -39,6 +37,7 @@ class D3FCChartElement extends HTMLElement { this._chart = chart; this._settings = this._configureSettings(this._settings, settings); + initialiseStyles(this._container, this._settings); this.draw(); if (window.navigator.userAgent.indexOf("Edge") > -1) { @@ -78,7 +77,8 @@ class D3FCChartElement extends HTMLElement { if (areArraysEqualSimple(oldValues, newValues)) return {...oldSettings, data: newSettings.data}; } this.remove(); - return newSettings; + const {colorStyles} = oldSettings || {}; + return {...newSettings, colorStyles}; } } diff --git a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js index 30232e62d7..22018f59d3 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js +++ b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js @@ -10,12 +10,10 @@ import * as d3 from "d3"; let initialised = false; -export const colorStyles = { - scheme: [] -}; +export const colorStyles = {}; -export const initialiseStyles = container => { - if (!initialised) { +export const initialiseStyles = (container, settings) => { + if (!initialised || !settings.colorStyles) { const selection = d3.select(container); const chart = selection.append("svg").attr("class", "chart"); @@ -31,19 +29,30 @@ export const initialiseStyles = container => { .append("circle") .attr("class", d => d); + const styles = { + scheme: [] + }; + testElements.each((d, i, nodes) => { const style = getComputedStyle(nodes[i]); - colorStyles[d] = style.getPropertyValue("fill"); + styles[d] = style.getPropertyValue("fill"); if (i > 0) { - colorStyles.scheme.push(colorStyles[d]); + styles.scheme.push(styles[d]); } }); - colorStyles.opacity = getOpacityFromColor(colorStyles.series); + styles.opacity = getOpacityFromColor(styles.series); chart.remove(); - initialised = true; + + if (!initialised) { + Object.keys(styles).forEach(p => { + colorStyles[p] = styles[p]; + }); + initialised = true; + } + settings.colorStyles = styles; } }; diff --git a/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js b/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js index 2e5dc0b0bb..11d00d748a 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/ohlcCandleSeries.js @@ -8,7 +8,6 @@ */ import * as fc from "d3fc"; import {colorScale, setOpacity} from "../series/seriesColors"; -import {colorStyles} from "./colorStyles"; const isUp = d => d.closeValue >= d.openValue; @@ -16,9 +15,12 @@ export function ohlcCandleSeries(settings, seriesCanvas, upColor) { const domain = upColor.domain(); const downColor = colorScale() .domain(domain) - .defaultColors([colorStyles["series-2"]]) + .settings(settings) + .defaultColors([settings.colorStyles["series-2"]]) .mapFunction(setOpacity(0.5))(); - const avgColor = colorScale().domain(domain)(); + const avgColor = colorScale() + .settings(settings) + .domain(domain)(); const series = seriesCanvas() .crossValue(d => d.crossValue) diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js b/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js index 84407ff605..315a3b4e0f 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js @@ -13,7 +13,9 @@ import {colorStyles} from "./colorStyles"; export function seriesColors(settings) { const col = settings.data && settings.data.length > 0 ? settings.data[0] : {}; const domain = Object.keys(col).filter(k => k !== "__ROW_PATH__"); - return colorScale().domain(domain)(); + return colorScale() + .settings(settings) + .domain(domain)(); } export function seriesColorsFromGroups(settings) { @@ -27,17 +29,22 @@ export function seriesColorsFromGroups(settings) { } } }); - return colorScale().domain(domain)(); + return colorScale() + .settings(settings) + .domain(domain)(); } export function colorScale() { let domain = null; - let defaultColors = [colorStyles.series]; + let defaultColors = null; let mapFunction = withOpacity; + let settings = {colorStyles}; const colors = () => { - if (defaultColors || domain.length > 1) { - const range = domain.length > 1 ? colorStyles.scheme : defaultColors; + const styles = settings.colorStyles; + const defaults = defaultColors || [styles.series]; + if (defaults || domain.length > 1) { + const range = domain.length > 1 ? styles.scheme : defaults; return d3.scaleOrdinal(range.map(mapFunction)).domain(domain); } return null; @@ -67,6 +74,14 @@ export function colorScale() { return colors; }; + colors.settings = (...args) => { + if (!args.length) { + return settings; + } + settings = args[0]; + return colors; + }; + return colors; } diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 72b41649c8..00268db40f 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -62,24 +62,13 @@ white-space: nowrap; } - & .bar { - fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); - } - & .line { - stroke: var(--d3fc-series, rgb(31, 119, 180)); - } - & .point, & .series { - fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); - stroke: var(--d3fc-series, rgb(31, 119, 180)); - } - & .area { + & .nearbyTip { fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); } - & .nearbyTip { + & .series { fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); } - & .series-1 { fill: var(--d3fc-series-1, #0366d6); } From 0fa2d19e0ed152b595330a74185502acce29a24c Mon Sep 17 00:00:00 2001 From: Andy Date: Tue, 26 Mar 2019 17:05:18 +0000 Subject: [PATCH 02/47] Use `ShadyCSS` component to get style variables directly This is the same as Highcharts currently uses. Avoids having to create svg and dom elements to get the styles. Requires that the default style variables are set in the perspective-viewer layer. --- .../src/js/series/colorStyles.js | 29 ++++++++----------- .../src/less/perspective-view.less | 14 +++++++++ .../src/themes/material.dark.less | 2 +- 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js index 22018f59d3..46ac991727 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js +++ b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js @@ -7,35 +7,23 @@ * */ -import * as d3 from "d3"; - let initialised = false; export const colorStyles = {}; export const initialiseStyles = (container, settings) => { if (!initialised || !settings.colorStyles) { - const selection = d3.select(container); - const chart = selection.append("svg").attr("class", "chart"); - const data = ["series"]; for (let n = 1; n <= 10; n++) { data.push(`series-${n}`); } - const testElements = chart - .selectAll("circle") - .data(data) - .enter() - .append("circle") - .attr("class", d => d); - const styles = { scheme: [] }; - testElements.each((d, i, nodes) => { - const style = getComputedStyle(nodes[i]); - styles[d] = style.getPropertyValue("fill"); + const computed = computedStyle(container); + data.forEach((d, i) => { + styles[d] = computed(`--d3fc-${d}`); if (i > 0) { styles.scheme.push(styles[d]); @@ -44,8 +32,6 @@ export const initialiseStyles = (container, settings) => { styles.opacity = getOpacityFromColor(styles.series); - chart.remove(); - if (!initialised) { Object.keys(styles).forEach(p => { colorStyles[p] = styles[p]; @@ -63,3 +49,12 @@ const getOpacityFromColor = color => { } return 1; }; + +const computedStyle = container => { + if (window.ShadyCSS) { + return d => window.ShadyCSS.getComputedStyleValue(container, d); + } else { + const containerStyles = getComputedStyle(container); + return d => containerStyles.getPropertyValue(d); + } +}; diff --git a/packages/perspective-viewer-d3fc/src/less/perspective-view.less b/packages/perspective-viewer-d3fc/src/less/perspective-view.less index 8689ba908e..60e6d11d3d 100644 --- a/packages/perspective-viewer-d3fc/src/less/perspective-view.less +++ b/packages/perspective-viewer-d3fc/src/less/perspective-view.less @@ -1,3 +1,17 @@ +:host { + --d3fc-series: rgba(31, 119, 180, 0.5); + --d3fc-series-1: #0366d6; + --d3fc-series-2: #ff7f0e; + --d3fc-series-3: #2ca02c; + --d3fc-series-4: #d62728; + --d3fc-series-5: #9467bd; + --d3fc-series-6: #8c564b; + --d3fc-series-7: #e377c2; + --d3fc-series-8: #7f7f7f; + --d3fc-series-9: #bcbd22; + --d3fc-series-10: #17becf; +} + :host([view=d3_xy_scatter]), :host([view=d3_candlestick]), :host([view=d3_ohlc]) { & #app { &.columns_horizontal #side_panel #inactive_columns { diff --git a/packages/perspective-viewer/src/themes/material.dark.less b/packages/perspective-viewer/src/themes/material.dark.less index 05a2929a2d..e1cd2e3f89 100644 --- a/packages/perspective-viewer/src/themes/material.dark.less +++ b/packages/perspective-viewer/src/themes/material.dark.less @@ -60,7 +60,7 @@ perspective-viewer { --d3fc-axis--lines: rgb(85, 85, 85); --d3fc-tooltip--background: #333333; --d3fc-tooltip--color: rgb(207, 216, 220); - --d3fc-series: rgb(31, 119, 180, 0.8); + --d3fc-series: rgba(31, 119, 180, 0.8); --d3fc-series-1: #0366d6; --d3fc-series-2: #ff7f0e; --d3fc-series-3: #2ca02c; From 69666385e7a2fbdfaed255c3f8c37cad0f922245 Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 27 Mar 2019 14:57:57 +0000 Subject: [PATCH 03/47] Implemented themable color ranges for heatmap and X/Y Implemented a special interpolator to connect up multiple two-color interpolators Heatmap color has to be reapplied because the d3fc one forces it to the data extent rather than the color range you've given it Found some d3 color utility functions --- .../src/js/series/colorStyles.js | 53 ++++++++++++++++--- .../src/js/series/heatmapSeries.js | 1 + .../src/js/series/seriesColors.js | 14 ++--- .../src/js/series/seriesRange.js | 20 +++++-- .../src/less/perspective-view.less | 19 +++++++ .../src/themes/material.dark.less | 21 ++++++++ 6 files changed, 106 insertions(+), 22 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js index 46ac991727..35132cfdac 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js +++ b/packages/perspective-viewer-d3fc/src/js/series/colorStyles.js @@ -7,6 +7,9 @@ * */ +import * as d3 from "d3"; +import * as gparser from "gradient-parser"; + let initialised = false; export const colorStyles = {}; @@ -18,7 +21,9 @@ export const initialiseStyles = (container, settings) => { } const styles = { - scheme: [] + scheme: [], + gradient: {}, + interpolator: {} }; const computed = computedStyle(container); @@ -32,6 +37,13 @@ export const initialiseStyles = (container, settings) => { styles.opacity = getOpacityFromColor(styles.series); + const gradients = ["full", "positive", "negative"]; + gradients.forEach(g => { + const gradient = computed(`--d3fc-gradient-${g}`); + styles.gradient[g] = parseGradient(gradient, styles.opacity); + styles.interpolator[g] = multiInterpolator(styles.gradient[g]); + }); + if (!initialised) { Object.keys(styles).forEach(p => { colorStyles[p] = styles[p]; @@ -43,11 +55,13 @@ export const initialiseStyles = (container, settings) => { }; const getOpacityFromColor = color => { - if (color.includes("rgba")) { - const rgbColors = color.substring(color.indexOf("(") + 1).split(","); - return parseFloat(rgbColors[3]); - } - return 1; + return d3.color(color).opacity; +}; + +const stepAsColor = (value, opacity) => { + const color = d3.color(`#${value}`); + color.opacity = opacity; + return color + ""; }; const computedStyle = container => { @@ -58,3 +72,30 @@ const computedStyle = container => { return d => containerStyles.getPropertyValue(d); } }; + +const parseGradient = (gradient, opacity) => + gparser + .parse(gradient)[0] + .colorStops.map(g => [g.length.value / 100, stepAsColor(g.value, opacity)]) + .sort((a, b) => a[0] - b[0]); + +const multiInterpolator = gradientPairs => { + // A new interpolator that calls through to a set of + // interpolators between each value/color pair + const interpolators = gradientPairs.slice(1).map((p, i) => d3.interpolate(gradientPairs[i][1], p[1])); + return value => { + const index = gradientPairs.findIndex((p, i) => i < gradientPairs.length - 1 && value <= gradientPairs[i + 1][0] && value > p[0]); + if (index === -1) { + if (value <= gradientPairs[0][0]) { + return gradientPairs[0][1]; + } + return gradientPairs[gradientPairs.length - 1][1]; + } + + const interpolator = interpolators[index]; + const [value1] = gradientPairs[index]; + const [value2] = gradientPairs[index + 1]; + + return interpolator((value - value1) / (value2 - value1)); + }; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/heatmapSeries.js b/packages/perspective-viewer-d3fc/src/js/series/heatmapSeries.js index 5bb8d88335..c0a706bdb8 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/heatmapSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/heatmapSeries.js @@ -14,6 +14,7 @@ export function heatmapSeries(settings, color) { series.decorate(selection => { tooltip().settings(settings)(selection); + selection.select("path").attr("fill", d => color(d.colorValue)); }); return fc diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js b/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js index 315a3b4e0f..fbc4d86810 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesColors.js @@ -95,16 +95,8 @@ export function withOpacity(color) { export function setOpacity(opacity) { return color => { - const toInt = (c, offset, length) => parseInt(c.substring(offset, offset + length) + (length === 1 ? "0" : ""), 16); - const colorsFromRGB = c => - c - .substring(c.indexOf("(") + 1) - .split(",") - .map(d => parseInt(d)) - .slice(0, 3); - const colorsFromHex = c => (c.length === 4 ? [toInt(c, 1, 1), toInt(c, 2, 1), toInt(c, 3, 1)] : [toInt(c, 1, 2), toInt(c, 3, 2), toInt(c, 5, 2)]); - - const colors = color.includes("rgb") ? colorsFromRGB(color) : colorsFromHex(color); - return opacity === 1 ? `rgb(${colors.join(",")})` : `rgba(${colors.join(",")},${opacity})`; + const decoded = d3.color(color); + decoded.opacity = opacity; + return decoded + ""; }; } diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js index 2fd0a0cbe4..9a6a338c5b 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js @@ -18,9 +18,19 @@ export function seriesLinearRange(settings, data, valueName) { } export function seriesColorRange(settings, data, valueName) { - return d3.scaleSequential(d3.interpolateViridis).domain( - domain() - .valueName(valueName) - .pad([0, 0])(data) - ); + let extent = domain() + .valueName(valueName) + .pad([0, 0])(data); + let interpolator = settings.colorStyles.interpolator.full; + + if (extent[0] >= 0) { + interpolator = settings.colorStyles.interpolator.positive; + } else if (extent[1] <= 0) { + interpolator = settings.colorStyles.interpolator.negative; + } else { + const maxVal = Math.max(-extent[0], extent[1]); + extent = [-maxVal, maxVal]; + } + + return d3.scaleSequential(interpolator).domain(extent); } diff --git a/packages/perspective-viewer-d3fc/src/less/perspective-view.less b/packages/perspective-viewer-d3fc/src/less/perspective-view.less index 60e6d11d3d..8d4c651e98 100644 --- a/packages/perspective-viewer-d3fc/src/less/perspective-view.less +++ b/packages/perspective-viewer-d3fc/src/less/perspective-view.less @@ -10,6 +10,25 @@ --d3fc-series-8: #7f7f7f; --d3fc-series-9: #bcbd22; --d3fc-series-10: #17becf; + --d3fc-gradient-full: linear-gradient( + #4d342f 0%, + #e4521b 22.5%, + #feeb65 42.5%, + #f0f0f0 50%, + #dcedc8 57.5%, + #42b3d5 67.5%, + #1a237e 100% + ); + --d3fc-gradient-positive: linear-gradient( + #dcedc8 0%, + #42b3d5 35%, + #1a237e 100% + ); + --d3fc-gradient-negative: linear-gradient( + #feeb65 100%, + #e4521b 70%, + #4d342f 0% + ); } :host([view=d3_xy_scatter]), :host([view=d3_candlestick]), :host([view=d3_ohlc]) { diff --git a/packages/perspective-viewer/src/themes/material.dark.less b/packages/perspective-viewer/src/themes/material.dark.less index e1cd2e3f89..610afa0630 100644 --- a/packages/perspective-viewer/src/themes/material.dark.less +++ b/packages/perspective-viewer/src/themes/material.dark.less @@ -71,6 +71,27 @@ perspective-viewer { --d3fc-series-8: #7f7f7f; --d3fc-series-9: #bcbd22; --d3fc-series-10: #17becf; + --d3fc-gradient-full: linear-gradient( + #4d342f 0%, + #e4521b 22.5%, + #feeb65 42.5%, + #f0f0f0 50%, + #dcedc8 57.5%, + #42b3d5 67.5%, + #1a237e 100% + ); + --d3fc-gradient-positive: linear-gradient( + #222222 0%, + #1a237e 35%, + #42b3d5 70%, + #dcedc8 100% + ); + --d3fc-gradient-negative: linear-gradient( + #feeb65 0%, + #e4521b 35%, + #4d342f 70%, + #222222 100% + ); --highcharts-heatmap-gradient-full: linear-gradient( #feeb65 0%, From b186e1638d2c23164109729561824a9c1db29ef4 Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Thu, 28 Mar 2019 11:40:21 +0000 Subject: [PATCH 04/47] update screenshot results with new colours --- .../test/results/results.json | 30 +++++++++---------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index c86cb1905a..ced5326214 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -1,5 +1,5 @@ { - "scatter.html/displays visible columns.": "ca9a2da7f3d7cd5edead17ddadfce123", + "scatter.html/displays visible columns.": "0eb7b2524b257079b9b29fb934576bc9", "line.html/shows a grid without any settings applied.": "156d4d4b96e95b4764701861846caafe", "line.html/pivots by a row.": "878a4e0cbcd03e022efcf5ceb708f41f", "line.html/pivots by two rows.": "517b6e07f59b615e2f23774fa5a1aec8", @@ -90,22 +90,22 @@ "area.html/highlights invalid filter.": "12353e9b69e92b8ac31b3117d57484c1", "area.html/sorts by an alpha column.": "b5b4bb0defd5d76d0984b6862e5fcbd2", "area.html/displays visible columns.": "992fe96f6920748310abd45b752f2d32", - "heatmap.html/shows a grid without any settings applied.": "d11a97a931740b6a45983568b7d8acbe", - "heatmap.html/pivots by a row.": "2615b96787c6aa8e15ae43f629e6608a", - "heatmap.html/pivots by two rows.": "8ea815f57e26222da41a8907dfd406ee", - "heatmap.html/pivots by a column.": "18dfd18d93f3f5ab6a436b9547cd565d", - "heatmap.html/pivots by a row and a column.": "3b7735731ecb65736f420e79bce2906c", - "heatmap.html/pivots by two rows and two columns.": "cbce4a7519c8c9cf41542b032eee37c0", - "heatmap.html/sorts by a hidden column.": "2730cd769e288f67c3e2d7e4684b7766", - "heatmap.html/sorts by a numeric column.": "d7682a33be5deeb67dca9d564b6460ea", - "heatmap.html/filters by a numeric column.": "bdd33b93079091297ff6d437c913e94f", - "heatmap.html/filters by a datetime column.": "2abcbde52f7798149c167e43471b2a24", - "heatmap.html/highlights invalid filter.": "d01bd4fb9e9e4005a946c90aaaddb57e", - "heatmap.html/sorts by an alpha column.": "9edf7f90f0b5cb72edb395f3c3420db9", - "heatmap.html/displays visible columns.": "76526dd190fae821de1bc873fccbb8fc", + "heatmap.html/shows a grid without any settings applied.": "bce441e933348ab1bb8986392e4909b9", + "heatmap.html/pivots by a row.": "f72086b690a408213d5cc4844dc72533", + "heatmap.html/pivots by two rows.": "969486a2c03f76ae3437850f55a83db0", + "heatmap.html/pivots by a column.": "1dfca59909039afb267bacddeeeb85ce", + "heatmap.html/pivots by a row and a column.": "946ffee69a918975fbfd4823245094b8", + "heatmap.html/pivots by two rows and two columns.": "57e1b294afd74c04009e14a3428e501e", + "heatmap.html/sorts by a hidden column.": "486189206f1e8db90772a6b3d05aab0f", + "heatmap.html/sorts by a numeric column.": "e2ddadc2dddc12fa336ed2837cd36f0c", + "heatmap.html/filters by a numeric column.": "75539031cc388a7f5ec6d468963c1b9a", + "heatmap.html/filters by a datetime column.": "7eb57bd0c2c731016c002435da4645fa", + "heatmap.html/highlights invalid filter.": "4b26893b6a811fda60c6befea2c661c1", + "heatmap.html/sorts by an alpha column.": "4390c31c4fd40385506aa8d56d8aebc8", + "heatmap.html/displays visible columns.": "4c89d56570fc7fd01a18485b8c2ffd0c", "candlestick.html/filter to date range.": "609fb900bb0ef5bbc8d98ab476abfd50", "candlestick.html/filter by a single instrument.": "36d43f5905d6e74cc622ee6d7be44894", "ohlc.html/filter by a single instrument.": "94d24188a2839ce1bd0b1dec71d20f33", "ohlc.html/filter to date range.": "3313f803361d232b1180e32b9a5d8c17", - "__GIT_COMMIT__": "e44d06594293371bcfef6592e6e7de30b1e562fc" + "__GIT_COMMIT__": "83251e20bf68703ac615e28a981c06c3ba8630a3" } \ No newline at end of file From 6e4ffee1e3f5871e94735d1a28d689edb9b13d78 Mon Sep 17 00:00:00 2001 From: Andy Date: Thu, 28 Mar 2019 15:22:38 +0000 Subject: [PATCH 05/47] Hide labels that would be clipped by the window bounds Check whether labels are outside of window bounds, and hide them. Also added support for fully rotating (90 degrees) when there are a very large number of labels, similar to Highcharts. --- .../src/js/axis/crossAxis.js | 84 ++++++++++++++----- .../js/legend/styling/draggableComponent.js | 6 +- .../src/js/plugin/root.js | 4 + .../src/js/tooltip/tooltip.js | 4 +- 4 files changed, 70 insertions(+), 28 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js index 62c4a5e115..d8dde4c535 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js @@ -11,6 +11,7 @@ import * as fc from "d3fc"; import minBandwidth from "./minBandwidth"; import withoutTicks from "./withoutTicks"; import {multiAxisBottom, multiAxisLeft} from "../d3fc/axis/multi-axis"; +import {getChartContainer} from "../plugin/root"; const AXIS_TYPES = { none: "none", @@ -145,13 +146,14 @@ export const axisFactory = settings => { .tickSizeInner(tickSizeInner) .tickSizeOuter(tickSizeOuter); } + if (orient !== "horizontal") axis.tickPadding(10); return axis; }; const decorate = (s, data, index) => { - const rotated = groupTickLayout[index].rotate; - hideOverlappingLabels(s, rotated); - if (orient === "horizontal") applyLabelRotation(s, rotated); + const rotation = groupTickLayout[index].rotation; + if (orient === "horizontal") applyLabelRotation(s, rotation); + hideOverlappingLabels(s, rotation); }; return { @@ -201,21 +203,26 @@ export const axisFactory = settings => { if (orient === "horizontal") { // x-axis may rotate labels and expand the available height - if (group.length * (maxLength * 6 + 10) > width - 100) { + if (group.length * 16 > width - 100) { + return { + size: maxLength * 5 + 10, + rotation: 90 + }; + } else if (group.length * (maxLength * 6 + 10) > width - 100) { return { size: maxLength * 3 + 20, - rotate: true + rotation: 45 }; } return { size: 25, - rotate: false + rotation: 0 }; } else { // y-axis size always based on label size return { size: maxLength * 5 + 10, - rotate: false + rotation: 0 }; } }; @@ -228,39 +235,70 @@ export const axisFactory = settings => { .map(c => parseInt(c)); const rectanglesOverlap = (r1, r2) => r1.x <= r2.x + r2.width && r2.x <= r1.x + r1.width && r1.y <= r2.y + r2.height && r2.y <= r1.y + r1.height; - const rotatedLabelsOverlap = (r1, r2) => r1.x + 14 > r2.x; + const rotatedLabelsOverlap = (r1, r2) => r1.x + r1.width + 14 > r2.x + r2.width; + const isOverlap = rotated ? rotatedLabelsOverlap : rectanglesOverlap; + + const rectangleContained = (r1, r2) => r1.x >= r2.x && r1.x + r1.width <= r2.x + r2.width && r1.y >= r2.y && r1.y + r1.height <= r2.y + r2.height; + // The bounds rect is the available screen space a label can fit into + const boundsRect = orient == "horizontal" ? getXAxisBoundsRect(s) : null; const previousRectangles = []; s.each((d, i, nodes) => { const tick = d3.select(nodes[i]); - const text = tick.select("text"); + // How the "tick" element is transformed (x/y) const transformCoords = getTransformCoords(tick.attr("transform")); - let rect = {}; - let overlap = false; - if (rotated) { - rect = {x: transformCoords[0], y: transformCoords[1]}; - overlap = previousRectangles.some(r => rotatedLabelsOverlap(r, rect)); - } else { - const textRect = text.node().getBBox(); - rect = {x: textRect.x + transformCoords[0], y: textRect.y + transformCoords[1], width: textRect.width, height: textRect.height}; - overlap = previousRectangles.some(r => rectanglesOverlap(r, rect)); - } + // Work out the actual rectanble the label occupies + const tickRect = tick.node().getBBox(); + const rect = {x: tickRect.x + transformCoords[0], y: tickRect.y + transformCoords[1], width: tickRect.width, height: tickRect.height}; + + const overlap = previousRectangles.some(r => isOverlap(r, rect)); - text.attr("visibility", overlap ? "hidden" : ""); - if (!overlap) { + // Test that it also fits into the screen space + const hidden = overlap || (boundsRect && !rectangleContained(rect, boundsRect)); + + tick.attr("visibility", hidden ? "hidden" : ""); + if (!hidden) { previousRectangles.push(rect); } }); }; - const applyLabelRotation = (s, rotate) => { + const getXAxisBoundsRect = s => { + const chart = getChartContainer(s.node()) + .getRootNode() + .querySelector(".cartesian-chart"); + const axis = chart.querySelector(".x-axis"); + + const chartRect = chart.getBoundingClientRect(); + const axisRect = axis.getBoundingClientRect(); + return { + x: chartRect.x - axisRect.x, + width: chartRect.width, + y: chartRect.y - axisRect.y, + height: chartRect.height + }; + }; + + const getLabelTransform = rotation => { + if (!rotation) { + return "translate(0, 8)"; + } + if (rotation < 60) { + return `rotate(-${rotation} 5 5)`; + } + return `rotate(-${rotation} 3 7)`; + }; + + const applyLabelRotation = (s, rotation) => { + const transform = getLabelTransform(rotation); + const anchor = rotation ? "end" : ""; s.each((d, i, nodes) => { const tick = d3.select(nodes[i]); const text = tick.select("text"); - text.attr("transform", rotate ? "rotate(-45 5 5)" : "translate(0, 8)").style("text-anchor", rotate ? "end" : ""); + text.attr("transform", transform).style("text-anchor", anchor); }); }; diff --git a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js index d0919da677..0c67a37daf 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/styling/draggableComponent.js @@ -9,7 +9,7 @@ import * as d3 from "d3"; import {isElementOverflowing} from "../../utils/utils"; -import {getChartElement} from "../../plugin/root"; +import {getChartContainer} from "../../plugin/root"; const margin = 10; const resizeForDraggingEvent = "resize.for-dragging"; @@ -63,7 +63,7 @@ function pinNodeToTopRight(node) { function isNodeInTopRight(node) { const nodeRect = node.getBoundingClientRect(); const containerRect = d3 - .select(getChartElement(node).getContainer()) + .select(getChartContainer(node)) .node() .getBoundingClientRect(); @@ -74,7 +74,7 @@ function isNodeInTopRight(node) { function enforceContainerBoundaries(innerNode, offsetX, offsetY) { const chartNodeRect = d3 - .select(getChartElement(innerNode).getContainer()) + .select(getChartContainer(innerNode)) .node() .getBoundingClientRect(); diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/root.js b/packages/perspective-viewer-d3fc/src/js/plugin/root.js index 1fdfb94cbc..5f9b7cc2b4 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/root.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/root.js @@ -10,3 +10,7 @@ export function getChartElement(element) { return element.getRootNode().host; } + +export function getChartContainer(element) { + return getChartElement(element).getContainer(); +} diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js b/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js index 00072a58e7..86cc5e6969 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js @@ -8,7 +8,7 @@ */ import {select} from "d3"; -import {getChartElement} from "../plugin/root"; +import {getChartContainer} from "../plugin/root"; import {getOrCreateElement, isElementOverflowing} from "../utils/utils"; import tooltipTemplate from "../../html/tooltip.html"; import {generateHtml} from "./generateHTML"; @@ -27,7 +27,7 @@ export const tooltip = () => { return; } - const container = select(getChartElement(node).getContainer()); + const container = select(getChartContainer(node)); tooltipDiv = getTooltipDiv(container); const showTip = (data, i, nodes) => { From 2b976ce8bcbcb938207b4462a2e6d9e47df798cb Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Mon, 1 Apr 2019 11:24:41 +0100 Subject: [PATCH 06/47] replace reference to _config with get_config --- packages/perspective-viewer-d3fc/src/js/plugin/plugin.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index c56b97a2e6..8aa44d53d7 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -35,7 +35,6 @@ function drawChart(chart) { // FIXME: super tight coupling to private viewer methods const aggregates = this._get_view_aggregates(); const hidden = this._get_view_hidden(aggregates); - const filter = this._view._config.filter; const [tschema, json, config] = await Promise.all([this._table.schema(), view.to_json(), view.get_config()]); if (task.cancelled) { @@ -43,6 +42,7 @@ function drawChart(chart) { } const row_pivots = config.row_pivot; const col_pivots = config.column_pivot; + const filter = config.filter; const filtered = row_pivots.length > 0 ? json.filter(col => col.__ROW_PATH__ && col.__ROW_PATH__.length == row_pivots.length) : json; const dataMap = !row_pivots.length ? (col, i) => ({...removeHiddenData(col, hidden), __ROW_PATH__: [i]}) : col => removeHiddenData(col, hidden); From aa331365ba3effb48b6733280bd9ecb87d4ec60d Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Mon, 1 Apr 2019 11:38:44 +0100 Subject: [PATCH 07/47] update screenshots where labels are now away from axis slightly --- .../test/results/results.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index ced5326214..59169b9d2b 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -39,11 +39,11 @@ "scatter.html/highlights invalid filter.": "db0b88d84e559975a100179ad1b313df", "scatter.html/sorts by an alpha column.": "d29c7922fe86891f65d491fc4c625d5d", "bar-x.html/shows a grid without any settings applied.": "4413e9006a9296d6ab42db93ae4c52ea", - "bar-x.html/pivots by a row.": "c6b5f172230edff664688c459e5dcb32", - "bar-x.html/pivots by two rows.": "978646b6fa69db583087dca978926c7b", + "bar-x.html/pivots by a row.": "dbdbd5b7daf4ec108e68592dbb2da59a", + "bar-x.html/pivots by two rows.": "af32d91b3da53b2b7a4e21eb270f3d92", "bar-x.html/pivots by a column.": "f604ed7e4dbad3267ab357d7a2d2b884", - "bar-x.html/pivots by a row and a column.": "33081f5c799607ac3ab36cf9866005c6", - "bar-x.html/pivots by two rows and two columns.": "2d5ecb5512b4f5d8468492a1a2782049", + "bar-x.html/pivots by a row and a column.": "d8fcc4a5381561cf63e787a640727379", + "bar-x.html/pivots by two rows and two columns.": "469bc3f48305e6e8a765d7ef90402635", "bar-x.html/sorts by a hidden column.": "d8ca2f614888d3c8da11ec85437f9b65", "bar-x.html/sorts by a numeric column.": "990a4c6667519c0bf54473ff446bcb3f", "bar-x.html/filters by a numeric column.": "4500c242e821c1e73495e455a83edb7e", @@ -92,10 +92,10 @@ "area.html/displays visible columns.": "992fe96f6920748310abd45b752f2d32", "heatmap.html/shows a grid without any settings applied.": "bce441e933348ab1bb8986392e4909b9", "heatmap.html/pivots by a row.": "f72086b690a408213d5cc4844dc72533", - "heatmap.html/pivots by two rows.": "969486a2c03f76ae3437850f55a83db0", - "heatmap.html/pivots by a column.": "1dfca59909039afb267bacddeeeb85ce", - "heatmap.html/pivots by a row and a column.": "946ffee69a918975fbfd4823245094b8", - "heatmap.html/pivots by two rows and two columns.": "57e1b294afd74c04009e14a3428e501e", + "heatmap.html/pivots by two rows.": "65a66c405a97d13888ef18d58dd9e828", + "heatmap.html/pivots by a column.": "24380b9a2088e1c5702f1a037ae7b520", + "heatmap.html/pivots by a row and a column.": "78ecf577b991670ea0b9520d3f3ec18c", + "heatmap.html/pivots by two rows and two columns.": "6138b0e17672528e80a0556a69c37cb2", "heatmap.html/sorts by a hidden column.": "486189206f1e8db90772a6b3d05aab0f", "heatmap.html/sorts by a numeric column.": "e2ddadc2dddc12fa336ed2837cd36f0c", "heatmap.html/filters by a numeric column.": "75539031cc388a7f5ec6d468963c1b9a", @@ -107,5 +107,5 @@ "candlestick.html/filter by a single instrument.": "36d43f5905d6e74cc622ee6d7be44894", "ohlc.html/filter by a single instrument.": "94d24188a2839ce1bd0b1dec71d20f33", "ohlc.html/filter to date range.": "3313f803361d232b1180e32b9a5d8c17", - "__GIT_COMMIT__": "83251e20bf68703ac615e28a981c06c3ba8630a3" + "__GIT_COMMIT__": "2b976ce8bcbcb938207b4462a2e6d9e47df798cb" } \ No newline at end of file From ef08487091d02b87c2ac97f151683f0b54e11d00 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 10:49:13 +0000 Subject: [PATCH 08/47] Added inital sunburst chart code --- .../src/js/axis/sunburstLabel.js | 7 ++ .../src/js/charts/charts.js | 3 +- .../src/js/charts/sunburst.js | 82 +++++++++++++++++++ .../src/js/data/treeData.js | 79 ++++++++++++++++++ .../src/js/interaction/clickHandler.js | 44 ++++++++++ .../src/js/series/arcSeries.js | 12 +++ 6 files changed, 226 insertions(+), 1 deletion(-) create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js create mode 100644 packages/perspective-viewer-d3fc/src/js/charts/sunburst.js create mode 100644 packages/perspective-viewer-d3fc/src/js/data/treeData.js create mode 100644 packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js create mode 100644 packages/perspective-viewer-d3fc/src/js/series/arcSeries.js diff --git a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js new file mode 100644 index 0000000000..bda978638a --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js @@ -0,0 +1,7 @@ +export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.05; + +export function labelTransform(d, radius) { + const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; + const y = ((d.y0 + d.y1) / 2) * radius; + return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; +} diff --git a/packages/perspective-viewer-d3fc/src/js/charts/charts.js b/packages/perspective-viewer-d3fc/src/js/charts/charts.js index d34b068cde..9ad16cb3ac 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/charts.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/charts.js @@ -16,7 +16,8 @@ import xyScatter from "./xy-scatter"; import heatmap from "./heatmap"; import ohlc from "./ohlc"; import candlestick from "./candlestick"; +import sunburst from "./sunburst"; -const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick]; +const chartClasses = [barChart, columnChart, lineChart, areaChart, yScatter, xyScatter, heatmap, ohlc, candlestick, sunburst]; export default chartClasses; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js new file mode 100644 index 0000000000..8c7ea51def --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -0,0 +1,82 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {select} from "d3"; +import {treeData} from "../data/treeData"; +import {clickHandler} from "../interaction/clickHandler"; +import {arc, arcVisible} from "../series/arcSeries"; +import {labelVisible, labelTransform} from "../axis/sunburstLabel"; + +function sunburstChart(sunburstElement, radius, {data, color}) { + data.each(d => (d.current = d)); + + const path = sunburstElement + .append("g") + .selectAll("path") + .data(data.descendants().slice(1)) + .join("path") + .attr("fill", d => color(d.data.color)) + .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) + .attr("d", d => arc(radius)(d.current)); + + const label = sunburstElement + .append("g") + .attr("pointer-events", "none") + .attr("text-anchor", "middle") + .style("user-select", "none") + .selectAll("text") + .data(data.descendants().slice(1)) + .join("text") + .attr("dy", "0.35em") + .attr("fill-opacity", d => +labelVisible(d.current)) + .attr("transform", d => labelTransform(d.current, radius)) + .text(d => d.data.name); + + const parentTitle = sunburstElement.append("text").attr("text-anchor", "middle"); + const parent = sunburstElement + .append("circle") + .attr("r", radius) + .attr("fill", "none") + .attr("pointer-events", "all") + .datum(data); + + const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius); + parent.on("click", onClick); + path.filter(d => d.children) + .style("cursor", "pointer") + .on("click", onClick); +} + +function sunburst(container, settings) { + const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); + const padding = 30; + const radius = (Math.min(containerWidth, containerHeight) - padding) / 6; + + treeData(settings).forEach(set => { + const sunburstSvg = container + .append("svg") + .style("width", "100%") + .style("height", containerHeight - padding / 2) + .append("g") + .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`); + + sunburstChart(sunburstSvg, radius, set); + }); +} +sunburst.plugin = { + type: "d3_sunburst", + name: "[D3] Sunburst", + max_size: 25000, + initial: { + type: "number", + count: 2 + } +}; + +export default sunburst; diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js new file mode 100644 index 0000000000..87af8ff2ad --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -0,0 +1,79 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import * as d3 from "d3"; +import {extentLinear} from "d3fc"; + +export function treeData(settings) { + const sets = {}; + settings.data.slice(1).forEach(d => { + const groups = d.__ROW_PATH__; + const splits = getSplitNames(d); + splits.forEach(split => { + let currentLevel; + if (!sets[split]) { + sets[split] = []; + } + currentLevel = sets[split]; + groups.forEach((group, i) => { + let element = currentLevel.find(e => e.name === group); + if (!element) { + element = {name: group, children: []}; + currentLevel.push(element); + } + if (settings.mainValues.length > 1) { + const colorValue = getDataValue(d, settings.mainValues[1], split); + element.color = element.color ? element.color + colorValue : colorValue; + } + if (i === groups.length - 1) { + element.name = groups.slice(-1)[0]; + if (groups.length === settings.crossValues.length) { + element.size = getDataValue(d, settings.mainValues[0], split); + } + } + currentLevel = element.children; + }); + }); + }); + + const data = Object.entries(sets).map(set => { + const tree = {name: "root", children: set[1]}; + const root = d3.hierarchy(tree).sum(d => d.size); + const color = treeColor(settings, set[0]); + return {split: set[0], data: d3.partition().size([2 * Math.PI, root.height + 1])(root), color}; + }); + + return data; +} + +const getDataValue = (d, aggregate, split) => (split.length ? d[`${split}|${aggregate.name}`] : d[aggregate.name]); + +function getSplitNames(d) { + const splits = []; + Object.keys(d).forEach(key => { + if (key !== "__ROW_PATH__") { + const splitValue = key + .split("|") + .slice(0, -1) + .join("|"); + if (!splits.includes(splitValue)) { + splits.push(splitValue); + } + } + }); + return splits; +} + +function treeColor(settings, split) { + if (settings.mainValues.length > 1) { + const colorDomain = extentLinear().accessors([d => getDataValue(d, settings.mainValues[1], split)])(settings.data); + return d3.scaleSequential(d3.interpolateRainbow).domain(colorDomain); + } + return () => "rgb(31, 119, 180)"; +} diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js new file mode 100644 index 0000000000..77d19661f3 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js @@ -0,0 +1,44 @@ +import {interpolate} from "d3"; +import {arc, arcVisible} from "../series/arcSeries"; +import {labelVisible, labelTransform} from "../axis/sunburstLabel"; + +export const clickHandler = (data, g, parent, parentTitle, path, label, radius) => p => { + if (p.parent) { + parent.datum(p.parent); + parent.style("cursor", "pointer"); + parentTitle.html(`⇪ ${p.parent.data.name}`); + } else { + parent.datum(data); + parent.style("cursor", "default"); + parentTitle.html(""); + } + data.each( + d => + (d.target = { + x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + y0: Math.max(0, d.y0 - p.depth), + y1: Math.max(0, d.y1 - p.depth) + }) + ); + + const t = g.transition().duration(750); + path.transition(t) + .tween("data", d => { + const i = interpolate(d.current, d.target); + return t => (d.current = i(t)); + }) + .filter(function(d) { + return +this.getAttribute("fill-opacity") || arcVisible(d.target); + }) + .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) + .attrTween("d", d => () => arc(radius)(d.current)); + + label + .filter(function(d) { + return +this.getAttribute("fill-opacity") || labelVisible(d.target); + }) + .transition(t) + .attr("fill-opacity", d => +labelVisible(d.target)) + .attrTween("transform", d => () => labelTransform(d.current, radius)); +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js new file mode 100644 index 0000000000..991ecb90a5 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js @@ -0,0 +1,12 @@ +import {arc as d3Arc} from "d3"; + +export const arc = radius => + d3Arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) + .padRadius(radius) + .innerRadius(d => d.y0 * radius) + .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)); + +export const arcVisible = d => d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; From 5b0ca723996197495a905cf13acecbff402b6a1e Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 10:54:25 +0000 Subject: [PATCH 09/47] Added scrolling to sunburst chart --- packages/perspective-viewer-d3fc/src/less/chart.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 00268db40f..3170d0c86b 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -23,6 +23,11 @@ padding-right: 165px; } + &.d3_sunburst { + padding: 0; + overflow: auto; + } + & .x-label { height: 1.2em !important; } From ed288237de1975f657c0b88a7a3764a01643f81a Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 12:23:11 +0000 Subject: [PATCH 10/47] Fixed color domain --- .../perspective-viewer-d3fc/src/js/data/treeData.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index 87af8ff2ad..002266af42 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -8,11 +8,10 @@ */ import * as d3 from "d3"; -import {extentLinear} from "d3fc"; export function treeData(settings) { const sets = {}; - settings.data.slice(1).forEach(d => { + settings.data.forEach(d => { const groups = d.__ROW_PATH__; const splits = getSplitNames(d); splits.forEach(split => { @@ -45,7 +44,7 @@ export function treeData(settings) { const data = Object.entries(sets).map(set => { const tree = {name: "root", children: set[1]}; const root = d3.hierarchy(tree).sum(d => d.size); - const color = treeColor(settings, set[0]); + const color = treeColor(settings, set); return {split: set[0], data: d3.partition().size([2 * Math.PI, root.height + 1])(root), color}; }); @@ -70,10 +69,11 @@ function getSplitNames(d) { return splits; } -function treeColor(settings, split) { +function treeColor(settings, [split, data]) { if (settings.mainValues.length > 1) { - const colorDomain = extentLinear().accessors([d => getDataValue(d, settings.mainValues[1], split)])(settings.data); - return d3.scaleSequential(d3.interpolateRainbow).domain(colorDomain); + const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); + const max = Math.max(...data.map(d => d.color)); + return d3.scaleSequential(d3.interpolateRainbow).domain([min, max]); } return () => "rgb(31, 119, 180)"; } From 74dfe44c80b927c061f0aa01eb26cd4c2e7dc490 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 12:32:32 +0000 Subject: [PATCH 11/47] Match color scheme to heatmap --- packages/perspective-viewer-d3fc/src/js/data/treeData.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index 002266af42..d6cd1fdf62 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -73,7 +73,7 @@ function treeColor(settings, [split, data]) { if (settings.mainValues.length > 1) { const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); const max = Math.max(...data.map(d => d.color)); - return d3.scaleSequential(d3.interpolateRainbow).domain([min, max]); + return d3.scaleSequential(d3.interpolateViridis).domain([min, max]); } return () => "rgb(31, 119, 180)"; } From 4bc6765f7943b714a9e2346e14fc3d5b3edd92dc Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 14:00:13 +0000 Subject: [PATCH 12/47] Removed unecessary import --- packages/perspective-viewer-d3fc/src/js/charts/sunburst.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 8c7ea51def..883e6044b9 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -7,7 +7,6 @@ * */ -import {select} from "d3"; import {treeData} from "../data/treeData"; import {clickHandler} from "../interaction/clickHandler"; import {arc, arcVisible} from "../series/arcSeries"; From 4dd2f3e545ed06d64dc8fee755c59c162a0e99f6 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Wed, 27 Mar 2019 14:59:36 +0000 Subject: [PATCH 13/47] Refactored sunburst chart creation --- .../src/js/charts/sunburst.js | 98 +++++++++---------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 883e6044b9..67cc2bd7b2 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -7,66 +7,66 @@ * */ +import {select} from "d3"; import {treeData} from "../data/treeData"; import {clickHandler} from "../interaction/clickHandler"; import {arc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; -function sunburstChart(sunburstElement, radius, {data, color}) { - data.each(d => (d.current = d)); - - const path = sunburstElement - .append("g") - .selectAll("path") - .data(data.descendants().slice(1)) - .join("path") - .attr("fill", d => color(d.data.color)) - .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) - .attr("d", d => arc(radius)(d.current)); - - const label = sunburstElement - .append("g") - .attr("pointer-events", "none") - .attr("text-anchor", "middle") - .style("user-select", "none") - .selectAll("text") - .data(data.descendants().slice(1)) - .join("text") - .attr("dy", "0.35em") - .attr("fill-opacity", d => +labelVisible(d.current)) - .attr("transform", d => labelTransform(d.current, radius)) - .text(d => d.data.name); - - const parentTitle = sunburstElement.append("text").attr("text-anchor", "middle"); - const parent = sunburstElement - .append("circle") - .attr("r", radius) - .attr("fill", "none") - .attr("pointer-events", "all") - .datum(data); - - const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius); - parent.on("click", onClick); - path.filter(d => d.children) - .style("cursor", "pointer") - .on("click", onClick); -} - function sunburst(container, settings) { const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); const padding = 30; const radius = (Math.min(containerWidth, containerHeight) - padding) / 6; - treeData(settings).forEach(set => { - const sunburstSvg = container - .append("svg") - .style("width", "100%") - .style("height", containerHeight - padding / 2) - .append("g") - .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`); + const sunburstContainer = container.selectAll("svg").data(treeData(settings)); + sunburstContainer.exit().remove(); + sunburstContainer + .enter() + .append("svg") + .style("width", "100%") + .style("height", containerHeight - padding / 2) + .append("g") + .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`) + .each(function({data, color}) { + const sunburstElement = select(this); + data.each(d => (d.current = d)); + + const path = sunburstElement + .append("g") + .selectAll("path") + .data(data.descendants().slice(1)) + .join("path") + .attr("fill", d => color(d.data.color)) + .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) + .attr("d", d => arc(radius)(d.current)); + + const label = sunburstElement + .append("g") + .attr("pointer-events", "none") + .attr("text-anchor", "middle") + .style("user-select", "none") + .selectAll("text") + .data(data.descendants().slice(1)) + .join("text") + .attr("dy", "0.35em") + .attr("fill-opacity", d => +labelVisible(d.current)) + .attr("transform", d => labelTransform(d.current, radius)) + .text(d => d.data.name); + + const parentTitle = sunburstElement.append("text").attr("text-anchor", "middle"); + const parent = sunburstElement + .append("circle") + .attr("r", radius) + .attr("fill", "none") + .attr("pointer-events", "all") + .datum(data); - sunburstChart(sunburstSvg, radius, set); - }); + const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius); + parent.on("click", onClick); + path.filter(d => d.children) + .style("cursor", "pointer") + .on("click", onClick); + }); } sunburst.plugin = { type: "d3_sunburst", From 6418bcbb30a7c5a94745472e09914b02f41cd9a6 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Thu, 28 Mar 2019 08:13:00 +0000 Subject: [PATCH 14/47] Renamed arc drawing function --- packages/perspective-viewer-d3fc/src/js/charts/sunburst.js | 4 ++-- .../src/js/interaction/clickHandler.js | 4 ++-- packages/perspective-viewer-d3fc/src/js/series/arcSeries.js | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 67cc2bd7b2..d9a5df04f1 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -10,7 +10,7 @@ import {select} from "d3"; import {treeData} from "../data/treeData"; import {clickHandler} from "../interaction/clickHandler"; -import {arc, arcVisible} from "../series/arcSeries"; +import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; function sunburst(container, settings) { @@ -38,7 +38,7 @@ function sunburst(container, settings) { .join("path") .attr("fill", d => color(d.data.color)) .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) - .attr("d", d => arc(radius)(d.current)); + .attr("d", d => drawArc(radius)(d.current)); const label = sunburstElement .append("g") diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js index 77d19661f3..0ca0c177c2 100644 --- a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js +++ b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js @@ -1,5 +1,5 @@ import {interpolate} from "d3"; -import {arc, arcVisible} from "../series/arcSeries"; +import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; export const clickHandler = (data, g, parent, parentTitle, path, label, radius) => p => { @@ -32,7 +32,7 @@ export const clickHandler = (data, g, parent, parentTitle, path, label, radius) return +this.getAttribute("fill-opacity") || arcVisible(d.target); }) .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) - .attrTween("d", d => () => arc(radius)(d.current)); + .attrTween("d", d => () => drawArc(radius)(d.current)); label .filter(function(d) { diff --git a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js index 991ecb90a5..64276608e5 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js @@ -1,7 +1,7 @@ -import {arc as d3Arc} from "d3"; +import {arc} from "d3"; -export const arc = radius => - d3Arc() +export const drawArc = radius => + arc() .startAngle(d => d.x0) .endAngle(d => d.x1) .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) From 291d0f81198922ef56f07b520d34ddfd2ad2f65b Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Thu, 28 Mar 2019 14:56:46 +0000 Subject: [PATCH 15/47] Support resizing --- .../src/js/charts/sunburst.js | 58 ++++++++++++------- .../src/js/interaction/clickHandler.js | 2 + .../src/less/chart.less | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index d9a5df04f1..f794cbc3aa 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -18,44 +18,58 @@ function sunburst(container, settings) { const padding = 30; const radius = (Math.min(containerWidth, containerHeight) - padding) / 6; - const sunburstContainer = container.selectAll("svg").data(treeData(settings)); - sunburstContainer.exit().remove(); - sunburstContainer - .enter() - .append("svg") - .style("width", "100%") - .style("height", containerHeight - padding / 2) - .append("g") + const sunburstSvg = container.selectAll("svg").data(treeData(settings), d => d.split); + sunburstSvg.exit().remove(); + + const sunburstEnter = sunburstSvg.enter().append("svg"); + const sunburstContainer = sunburstEnter.append("g").attr("class", "sunburst"); + sunburstContainer.append("circle"); + sunburstContainer.append("text"); + sunburstEnter + .merge(sunburstSvg) + .style("width", containerWidth) + .style("height", containerHeight) + .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`) .each(function({data, color}) { const sunburstElement = select(this); data.each(d => (d.current = d)); - const path = sunburstElement + const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); + const segmentEnter = segment + .enter() .append("g") - .selectAll("path") - .data(data.descendants().slice(1)) - .join("path") + .attr("class", "segment") + .attr("text-anchor", "middle"); + segmentEnter.append("path"); + segmentEnter.append("text"); + + const segmentMerge = segmentEnter.merge(segment); + + const path = segmentMerge + .select("path") .attr("fill", d => color(d.data.color)) .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) + .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) .attr("d", d => drawArc(radius)(d.current)); - const label = sunburstElement - .append("g") - .attr("pointer-events", "none") - .attr("text-anchor", "middle") - .style("user-select", "none") - .selectAll("text") - .data(data.descendants().slice(1)) - .join("text") + const label = segmentMerge + .select("text") .attr("dy", "0.35em") + .attr("user-select", "none") + .attr("pointer-events", "none") .attr("fill-opacity", d => +labelVisible(d.current)) .attr("transform", d => labelTransform(d.current, radius)) .text(d => d.data.name); - const parentTitle = sunburstElement.append("text").attr("text-anchor", "middle"); + const parentTitle = sunburstElement + .select("text") + .attr("text-anchor", "middle") + .attr("user-select", "none") + .attr("pointer-events", "none"); const parent = sunburstElement - .append("circle") + .select("circle") .attr("r", radius) .attr("fill", "none") .attr("pointer-events", "all") diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js index 0ca0c177c2..a9c0c73340 100644 --- a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js +++ b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js @@ -32,6 +32,8 @@ export const clickHandler = (data, g, parent, parentTitle, path, label, radius) return +this.getAttribute("fill-opacity") || arcVisible(d.target); }) .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) + .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) .attrTween("d", d => () => drawArc(radius)(d.current)); label diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 3170d0c86b..50f120d77f 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -25,7 +25,7 @@ &.d3_sunburst { padding: 0; - overflow: auto; + overflow: none; } & .x-label { From b1e4f77750247760fba3f8684e5118580b81ad59 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Thu, 28 Mar 2019 15:01:01 +0000 Subject: [PATCH 16/47] Refactored invariant enter attributes --- .../src/js/charts/sunburst.js | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index f794cbc3aa..a4a5dfb3f0 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -23,8 +23,17 @@ function sunburst(container, settings) { const sunburstEnter = sunburstSvg.enter().append("svg"); const sunburstContainer = sunburstEnter.append("g").attr("class", "sunburst"); - sunburstContainer.append("circle"); - sunburstContainer.append("text"); + sunburstContainer + .append("circle") + .attr("fill", "none") + .attr("pointer-events", "all"); + + sunburstContainer + .append("text") + .attr("text-anchor", "middle") + .attr("user-select", "none") + .attr("pointer-events", "none"); + sunburstEnter .merge(sunburstSvg) .style("width", containerWidth) @@ -41,8 +50,13 @@ function sunburst(container, settings) { .append("g") .attr("class", "segment") .attr("text-anchor", "middle"); + segmentEnter.append("path"); - segmentEnter.append("text"); + segmentEnter + .append("text") + .attr("dy", "0.35em") + .attr("user-select", "none") + .attr("pointer-events", "none"); const segmentMerge = segmentEnter.merge(segment); @@ -56,23 +70,14 @@ function sunburst(container, settings) { const label = segmentMerge .select("text") - .attr("dy", "0.35em") - .attr("user-select", "none") - .attr("pointer-events", "none") .attr("fill-opacity", d => +labelVisible(d.current)) .attr("transform", d => labelTransform(d.current, radius)) .text(d => d.data.name); - const parentTitle = sunburstElement - .select("text") - .attr("text-anchor", "middle") - .attr("user-select", "none") - .attr("pointer-events", "none"); + const parentTitle = sunburstElement.select("text"); const parent = sunburstElement .select("circle") .attr("r", radius) - .attr("fill", "none") - .attr("pointer-events", "all") .datum(data); const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius); From 64f158b6c643adfae105bce03608757b58f4b9df Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Thu, 28 Mar 2019 15:48:34 +0000 Subject: [PATCH 17/47] Support streaming data --- .../src/js/charts/sunburst.js | 14 ++++++++++---- .../src/js/interaction/clickHandler.js | 5 +++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index a4a5dfb3f0..6639176df9 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -40,7 +40,7 @@ function sunburst(container, settings) { .style("height", containerHeight) .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`) - .each(function({data, color}) { + .each(function({split, data, color}) { const sunburstElement = select(this); data.each(d => (d.current = d)); @@ -80,11 +80,17 @@ function sunburst(container, settings) { .attr("r", radius) .datum(data); - const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius); - parent.on("click", onClick); + const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius, split, settings); + if (settings.sunburstLevel) { + const currentLevel = data.descendants().find(d => d.data.name === settings.sunburstLevel[split]); + currentLevel && onClick(currentLevel, true); + } else { + settings.sunburstLevel = {}; + } + parent.on("click", d => onClick(d, false)); path.filter(d => d.children) .style("cursor", "pointer") - .on("click", onClick); + .on("click", d => onClick(d, false)); }); } sunburst.plugin = { diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js index a9c0c73340..dcc9ea9346 100644 --- a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js +++ b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js @@ -2,7 +2,8 @@ import {interpolate} from "d3"; import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; -export const clickHandler = (data, g, parent, parentTitle, path, label, radius) => p => { +export const clickHandler = (data, g, parent, parentTitle, path, label, radius, split, settings) => (p, skipTransition) => { + settings.sunburstLevel[split] = p.data.name; if (p.parent) { parent.datum(p.parent); parent.style("cursor", "pointer"); @@ -22,7 +23,7 @@ export const clickHandler = (data, g, parent, parentTitle, path, label, radius) }) ); - const t = g.transition().duration(750); + const t = g.transition().duration(skipTransition ? 0 : 750); path.transition(t) .tween("data", d => { const i = interpolate(d.current, d.target); From 75273968ec35bd021a1bc99bf4ed40b61e28d74e Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 09:28:34 +0000 Subject: [PATCH 18/47] Add support for side-by-side splits view --- .../src/js/charts/sunburst.js | 26 ++++++++++++++----- .../src/less/chart.less | 1 - 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 6639176df9..a2cdfa1a14 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -14,11 +14,25 @@ import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; function sunburst(container, settings) { - const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); + const sunburstData = treeData(settings); + const containerRect = container.node().getBoundingClientRect(); + const containerWidth = containerRect.width; + const containerHeight = containerRect.height; + const padding = 30; - const radius = (Math.min(containerWidth, containerHeight) - padding) / 6; + const minSize = 400; + const cols = sunburstData.length === 1 ? 1 : Math.floor(containerWidth / minSize); + const rows = sunburstData.length / cols; + let scrollMargin = 0; + if ((containerHeight / cols) * rows > containerHeight) { + scrollMargin = 17; + container.style("overflow-y", "auto"); + } else { + container.style("overflow-y", "hidden"); + } - const sunburstSvg = container.selectAll("svg").data(treeData(settings), d => d.split); + const radius = (Math.min(containerWidth, containerHeight) - padding) / 6 / cols - scrollMargin; + const sunburstSvg = container.selectAll("svg").data(sunburstData, d => d.split); sunburstSvg.exit().remove(); const sunburstEnter = sunburstSvg.enter().append("svg"); @@ -36,10 +50,10 @@ function sunburst(container, settings) { sunburstEnter .merge(sunburstSvg) - .style("width", containerWidth) - .style("height", containerHeight) + .style("width", containerWidth / cols - scrollMargin) + .style("height", containerHeight / cols) .select("g.sunburst") - .attr("transform", `translate(${containerWidth / 2}, ${containerHeight / 2})`) + .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) .each(function({split, data, color}) { const sunburstElement = select(this); data.each(d => (d.current = d)); diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 50f120d77f..92e550c425 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -25,7 +25,6 @@ &.d3_sunburst { padding: 0; - overflow: none; } & .x-label { From 4f8bb05875e501f12816854a34732896d40ed475 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 11:16:33 +0000 Subject: [PATCH 19/47] Wrap svgs in divs and use grid layout --- .../src/js/charts/sunburst.js | 39 +++++++++++-------- .../src/less/chart.less | 2 + 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index a2cdfa1a14..9b513310f5 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -19,24 +19,26 @@ function sunburst(container, settings) { const containerWidth = containerRect.width; const containerHeight = containerRect.height; - const padding = 30; const minSize = 400; const cols = sunburstData.length === 1 ? 1 : Math.floor(containerWidth / minSize); - const rows = sunburstData.length / cols; - let scrollMargin = 0; - if ((containerHeight / cols) * rows > containerHeight) { - scrollMargin = 17; - container.style("overflow-y", "auto"); - } else { - container.style("overflow-y", "hidden"); - } + const rows = Math.ceil(sunburstData.length / cols); + container.style("grid-template-columns", `repeat(${cols}, ${containerWidth / cols}px)`); + container.style("grid-template-rows", `repeat(${rows}, ${containerHeight / cols}px)`); + + const sunburstDiv = container.selectAll("div").data(treeData(settings), d => d.split); + sunburstDiv.exit().remove(); + + const sunburstEnter = sunburstDiv + .enter() + .append("div") + .style("overflow", "hidden"); - const radius = (Math.min(containerWidth, containerHeight) - padding) / 6 / cols - scrollMargin; - const sunburstSvg = container.selectAll("svg").data(sunburstData, d => d.split); - sunburstSvg.exit().remove(); + const sunburstContainer = sunburstEnter + .append("svg") + .style("overflow", "visible") + .append("g") + .attr("class", "sunburst"); - const sunburstEnter = sunburstSvg.enter().append("svg"); - const sunburstContainer = sunburstEnter.append("g").attr("class", "sunburst"); sunburstContainer .append("circle") .attr("fill", "none") @@ -49,13 +51,16 @@ function sunburst(container, settings) { .attr("pointer-events", "none"); sunburstEnter - .merge(sunburstSvg) - .style("width", containerWidth / cols - scrollMargin) - .style("height", containerHeight / cols) + .merge(sunburstDiv) + .select("svg") + .style("height", "80%") + .style("width", "80%") .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) .each(function({split, data, color}) { const sunburstElement = select(this); + const {width, height} = this.parentNode.getBoundingClientRect(); + const radius = Math.min(width, height) / 6; data.each(d => (d.current = d)); const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 92e550c425..b0b26143c0 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -24,7 +24,9 @@ } &.d3_sunburst { + display: inline-grid; padding: 0; + overflow-y: auto; } & .x-label { From da02cb9691263e7bc1a89d33bc0877c9eca51c18 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 11:43:39 +0000 Subject: [PATCH 20/47] Added titles to splits --- .../src/js/axis/sunburstLabel.js | 2 +- .../src/js/charts/sunburst.js | 17 +++++++++++++---- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js index bda978638a..774d6df1ed 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js @@ -1,4 +1,4 @@ -export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.05; +export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; export function labelTransform(d, radius) { const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 9b513310f5..aa7798332e 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -39,6 +39,11 @@ function sunburst(container, settings) { .append("g") .attr("class", "sunburst"); + sunburstContainer + .append("text") + .attr("class", "title") + .attr("text-anchor", "middle"); + sunburstContainer .append("circle") .attr("fill", "none") @@ -46,6 +51,7 @@ function sunburst(container, settings) { sunburstContainer .append("text") + .attr("class", "parent") .attr("text-anchor", "middle") .attr("user-select", "none") .attr("pointer-events", "none"); @@ -53,14 +59,17 @@ function sunburst(container, settings) { sunburstEnter .merge(sunburstDiv) .select("svg") - .style("height", "80%") - .style("width", "80%") + .style("height", "100%") + .style("width", "100%") .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) .each(function({split, data, color}) { const sunburstElement = select(this); const {width, height} = this.parentNode.getBoundingClientRect(); - const radius = Math.min(width, height) / 6; + const title = sunburstElement.select("text.title").text(split); + title.attr("transform", `translate(0, -${height / 2 - sunburstElement.node().getBoundingClientRect().height})`); + + const radius = (Math.min(width, height) - 100) / 6; data.each(d => (d.current = d)); const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); @@ -93,7 +102,7 @@ function sunburst(container, settings) { .attr("transform", d => labelTransform(d.current, radius)) .text(d => d.data.name); - const parentTitle = sunburstElement.select("text"); + const parentTitle = sunburstElement.select("text.parent"); const parent = sunburstElement .select("circle") .attr("r", radius) From c6cccd0f757ebe94895dbfdfaad7f7fcbb6e423a Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 11:56:25 +0000 Subject: [PATCH 21/47] Moved some styling into css --- .../src/js/charts/sunburst.js | 31 +++---------------- .../src/less/chart.less | 15 +++++++++ 2 files changed, 20 insertions(+), 26 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index aa7798332e..8cc764016b 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -28,39 +28,24 @@ function sunburst(container, settings) { const sunburstDiv = container.selectAll("div").data(treeData(settings), d => d.split); sunburstDiv.exit().remove(); - const sunburstEnter = sunburstDiv - .enter() - .append("div") - .style("overflow", "hidden"); + const sunburstEnter = sunburstDiv.enter().append("div"); const sunburstContainer = sunburstEnter .append("svg") - .style("overflow", "visible") .append("g") .attr("class", "sunburst"); - sunburstContainer - .append("text") - .attr("class", "title") - .attr("text-anchor", "middle"); + sunburstContainer.append("text").attr("class", "title"); sunburstContainer .append("circle") .attr("fill", "none") .attr("pointer-events", "all"); - sunburstContainer - .append("text") - .attr("class", "parent") - .attr("text-anchor", "middle") - .attr("user-select", "none") - .attr("pointer-events", "none"); - + sunburstContainer.append("text").attr("class", "parent"); sunburstEnter .merge(sunburstDiv) .select("svg") - .style("height", "100%") - .style("width", "100%") .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) .each(function({split, data, color}) { @@ -76,16 +61,10 @@ function sunburst(container, settings) { const segmentEnter = segment .enter() .append("g") - .attr("class", "segment") - .attr("text-anchor", "middle"); + .attr("class", "segment"); segmentEnter.append("path"); - segmentEnter - .append("text") - .attr("dy", "0.35em") - .attr("user-select", "none") - .attr("pointer-events", "none"); - + segmentEnter.append("text").attr("dy", "0.35em"); const segmentMerge = segmentEnter.merge(segment); const path = segmentMerge diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index b0b26143c0..9dda8258bc 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -27,6 +27,21 @@ display: inline-grid; padding: 0; overflow-y: auto; + + & div { + overflow: hidden; + } + + & svg { + width: 100%; + height: 100%; + } + + & text { + text-anchor: middle; + user-select: none; + pointer-events: none; + } } & .x-label { From 75c6d9557f48fd6e65f9767105fcb904f1643a95 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 14:01:51 +0000 Subject: [PATCH 22/47] Fixed title on re-size --- packages/perspective-viewer-d3fc/src/js/charts/sunburst.js | 2 +- packages/perspective-viewer-d3fc/src/less/chart.less | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 8cc764016b..1e5d5e7e13 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -52,7 +52,7 @@ function sunburst(container, settings) { const sunburstElement = select(this); const {width, height} = this.parentNode.getBoundingClientRect(); const title = sunburstElement.select("text.title").text(split); - title.attr("transform", `translate(0, -${height / 2 - sunburstElement.node().getBoundingClientRect().height})`); + title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); const radius = (Math.min(width, height) - 100) / 6; data.each(d => (d.current = d)); diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 9dda8258bc..8f7e6edf72 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -41,6 +41,10 @@ text-anchor: middle; user-select: none; pointer-events: none; + + &.title { + dominant-baseline: hanging; + } } } From 156d426f36d4e80e2a6e7a4e03091056be8d32e8 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Fri, 29 Mar 2019 14:29:35 +0000 Subject: [PATCH 23/47] Added legend to sunburst --- .../src/js/charts/sunburst.js | 21 +++++++++++++------ .../src/less/chart.less | 11 +++++++++- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 1e5d5e7e13..91287a6041 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -12,14 +12,13 @@ import {treeData} from "../data/treeData"; import {clickHandler} from "../interaction/clickHandler"; import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; +import {colorRangeLegend} from "../legend/colorRangeLegend"; function sunburst(container, settings) { const sunburstData = treeData(settings); - const containerRect = container.node().getBoundingClientRect(); - const containerWidth = containerRect.width; - const containerHeight = containerRect.height; + const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); - const minSize = 400; + const minSize = 500; const cols = sunburstData.length === 1 ? 1 : Math.floor(containerWidth / minSize); const rows = Math.ceil(sunburstData.length / cols); container.style("grid-template-columns", `repeat(${cols}, ${containerWidth / cols}px)`); @@ -50,7 +49,9 @@ function sunburst(container, settings) { .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) .each(function({split, data, color}) { const sunburstElement = select(this); - const {width, height} = this.parentNode.getBoundingClientRect(); + const svgNode = this.parentNode; + const {width, height} = svgNode.getBoundingClientRect(); + const title = sunburstElement.select("text.title").text(split); title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); @@ -64,7 +65,10 @@ function sunburst(container, settings) { .attr("class", "segment"); segmentEnter.append("path"); - segmentEnter.append("text").attr("dy", "0.35em"); + segmentEnter + .append("text") + .attr("class", "segment") + .attr("dy", "0.35em"); const segmentMerge = segmentEnter.merge(segment); const path = segmentMerge @@ -98,6 +102,11 @@ function sunburst(container, settings) { path.filter(d => d.children) .style("cursor", "pointer") .on("click", d => onClick(d, false)); + + const legend = colorRangeLegend().scale(color); + select(svgNode.parentNode) + .call(legend) + .select("div.legend-container"); }); } sunburst.plugin = { diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 8f7e6edf72..4b0c117c8c 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -37,7 +37,9 @@ height: 100%; } - & text { + & text.title, + & text.segment, + & text.parent { text-anchor: middle; user-select: none; pointer-events: none; @@ -46,6 +48,13 @@ dominant-baseline: hanging; } } + + .legend-container { + display: inline-flex; + top: unset; + right: unset; + transform: translate(-100px, 5px); + } } & .x-label { From a47b0b7455ad9eb88bb6626605744240b49a4953 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 09:11:28 +0100 Subject: [PATCH 24/47] Added label overflow ellipses --- .../src/js/axis/sunburstLabel.js | 25 +++++++++++++++++++ .../src/js/charts/sunburst.js | 7 ++++-- .../src/js/interaction/clickHandler.js | 9 +++++++ .../src/js/series/arcSeries.js | 9 +++++++ 4 files changed, 48 insertions(+), 2 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js index 774d6df1ed..f9472c3063 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js @@ -1,3 +1,14 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {select} from "d3"; + export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; export function labelTransform(d, radius) { @@ -5,3 +16,17 @@ export function labelTransform(d, radius) { const y = ((d.y0 + d.y1) / 2) * radius; return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; } + +export function cropLabel(d, targetWidth) { + let actualWidth = this.getBBox().width; + if (actualWidth > targetWidth) { + let labelText = d.data.name; + const textSelection = select(this); + while (actualWidth > targetWidth) { + labelText = labelText.substring(0, labelText.length - 1); + textSelection.text(() => labelText); + actualWidth = this.getBBox().width; + } + textSelection.text(() => `${labelText.substring(0, labelText.length - 3).replace(/\s+$/, "")}...`); + } +} diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 91287a6041..1a830cb461 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -11,7 +11,7 @@ import {select} from "d3"; import {treeData} from "../data/treeData"; import {clickHandler} from "../interaction/clickHandler"; import {drawArc, arcVisible} from "../series/arcSeries"; -import {labelVisible, labelTransform} from "../axis/sunburstLabel"; +import {labelVisible, labelTransform, cropLabel} from "../axis/sunburstLabel"; import {colorRangeLegend} from "../legend/colorRangeLegend"; function sunburst(container, settings) { @@ -83,7 +83,10 @@ function sunburst(container, settings) { .select("text") .attr("fill-opacity", d => +labelVisible(d.current)) .attr("transform", d => labelTransform(d.current, radius)) - .text(d => d.data.name); + .text(d => d.data.name) + .each(function(d) { + cropLabel.call(this, d, radius); + }); const parentTitle = sunburstElement.select("text.parent"); const parent = sunburstElement diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js index dcc9ea9346..2f205cad15 100644 --- a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js +++ b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js @@ -1,3 +1,12 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + import {interpolate} from "d3"; import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform} from "../axis/sunburstLabel"; diff --git a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js index 64276608e5..a22c841cc0 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js @@ -1,3 +1,12 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + import {arc} from "d3"; export const drawArc = radius => From 1fc907ed1e19288044eb91b7039978bc56e29cf7 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 10:45:02 +0100 Subject: [PATCH 25/47] Added tooltip to sunburst --- .../src/js/charts/sunburst.js | 22 ++++++++++++++++--- .../src/js/tooltip/selectionData.js | 2 +- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 1a830cb461..88a1903287 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -13,6 +13,7 @@ import {clickHandler} from "../interaction/clickHandler"; import {drawArc, arcVisible} from "../series/arcSeries"; import {labelVisible, labelTransform, cropLabel} from "../axis/sunburstLabel"; import {colorRangeLegend} from "../legend/colorRangeLegend"; +import {tooltip} from "../tooltip/tooltip"; function sunburst(container, settings) { const sunburstData = treeData(settings); @@ -24,10 +25,13 @@ function sunburst(container, settings) { container.style("grid-template-columns", `repeat(${cols}, ${containerWidth / cols}px)`); container.style("grid-template-rows", `repeat(${rows}, ${containerHeight / cols}px)`); - const sunburstDiv = container.selectAll("div").data(treeData(settings), d => d.split); + const sunburstDiv = container.selectAll("div.sunburst-container").data(treeData(settings), d => d.split); sunburstDiv.exit().remove(); - const sunburstEnter = sunburstDiv.enter().append("div"); + const sunburstEnter = sunburstDiv + .enter() + .append("div") + .attr("class", "sunburst-container"); const sunburstContainer = sunburstEnter .append("svg") @@ -56,7 +60,17 @@ function sunburst(container, settings) { title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); const radius = (Math.min(width, height) - 100) / 6; - data.each(d => (d.current = d)); + data.each(d => { + d.current = d; + d.mainValues = settings.mainValues.length === 1 ? d.value : [d.value, d.data.color]; + d.crossValue = d + .ancestors() + .slice(0, -1) + .reverse() + .map(cross => cross.data.name) + .join("|"); + d.key = split; + }); const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); const segmentEnter = segment @@ -110,6 +124,8 @@ function sunburst(container, settings) { select(svgNode.parentNode) .call(legend) .select("div.legend-container"); + + tooltip().settings(settings)(sunburstElement.selectAll("g.segment")); }); } sunburst.plugin = { diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js index 8bc3578f7a..de9593d87f 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/selectionData.js @@ -22,7 +22,7 @@ function toValue(type, value) { export function getGroupValues(data, settings) { if (settings.crossValues.length === 0) return []; const groupValues = (data.crossValue.split ? data.crossValue.split("|") : [data.crossValue]) || [data.key]; - return settings.crossValues.map((cross, i) => ({name: cross.name, value: toValue(cross.type, groupValues[i])})); + return groupValues.map((cross, i) => ({name: settings.crossValues[i].name, value: toValue(settings.crossValues[i].type, cross)})); } export function getSplitValues(data, settings) { From e3ecae0a4d8432e02826bc20a3e8165efc8c8cdc Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 12:23:45 +0100 Subject: [PATCH 26/47] Integrated series color range function into sunburst --- .../perspective-viewer-d3fc/src/js/data/treeData.js | 3 ++- .../src/js/series/seriesRange.js | 10 ++++++---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index d6cd1fdf62..abf320454b 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -8,6 +8,7 @@ */ import * as d3 from "d3"; +import {seriesColorRange} from "../series/seriesRange"; export function treeData(settings) { const sets = {}; @@ -73,7 +74,7 @@ function treeColor(settings, [split, data]) { if (settings.mainValues.length > 1) { const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); const max = Math.max(...data.map(d => d.color)); - return d3.scaleSequential(d3.interpolateViridis).domain([min, max]); + return seriesColorRange(settings, null, null, [min, max]); } return () => "rgb(31, 119, 180)"; } diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js index 9a6a338c5b..bf47cd2fc0 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js @@ -17,10 +17,12 @@ export function seriesLinearRange(settings, data, valueName) { ); } -export function seriesColorRange(settings, data, valueName) { - let extent = domain() - .valueName(valueName) - .pad([0, 0])(data); +export function seriesColorRange(settings, data, valueName, customExtent) { + let extent = + customExtent || + domain() + .valueName(valueName) + .pad([0, 0])(data); let interpolator = settings.colorStyles.interpolator.full; if (extent[0] >= 0) { From f4bf7b84a527435892411f49c436dd3f84d8ac1e Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 12:42:13 +0100 Subject: [PATCH 27/47] Fixed tooltip on scroll --- packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js b/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js index 86cc5e6969..19ccf7bfde 100644 --- a/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js +++ b/packages/perspective-viewer-d3fc/src/js/tooltip/tooltip.js @@ -83,7 +83,7 @@ function showTooltip(containerNode, barNode, tooltipDiv) { const barRect = barNode.getBoundingClientRect(); const left = barRect.left + barRect.width / 2 - containerRect.left; - const top = barRect.top - containerRect.top; + const top = barRect.top - containerRect.top + containerNode.scrollTop; tooltipDiv .style("left", `${left}px`) From 14faea098ef3347b012e0457c3fbc16d0b9571fa Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 13:02:09 +0100 Subject: [PATCH 28/47] Support dark theme --- packages/perspective-viewer-d3fc/src/less/chart.less | 5 +++-- .../perspective-viewer/src/themes/material.dark.less | 11 ++++------- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 4b0c117c8c..ecbcd89089 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -43,6 +43,7 @@ text-anchor: middle; user-select: none; pointer-events: none; + fill: var(--d3fc-sunburst--labels, rgb(51, 51, 51)); &.title { dominant-baseline: hanging; @@ -174,7 +175,7 @@ } & text { - fill: var(--d3fc-legend-text, rgb(51, 51, 51)); + fill: var(--d3fc-legend--text, rgb(51, 51, 51)); } & .label { @@ -184,7 +185,7 @@ } & #legend-axis path { - stroke: var(--d3fc-legend-text, rgb(51, 51, 51)); + stroke: var(--d3fc-legend--text, rgb(51, 51, 51)); } & .legend-controls { diff --git a/packages/perspective-viewer/src/themes/material.dark.less b/packages/perspective-viewer/src/themes/material.dark.less index 610afa0630..8780bc8479 100644 --- a/packages/perspective-viewer/src/themes/material.dark.less +++ b/packages/perspective-viewer/src/themes/material.dark.less @@ -25,7 +25,7 @@ perspective-viewer { --column-drop-target--background: @blue900; --active-column-selector--color: @lightblue600; --column_type-float--color: @lightblue600; - --column_type-string--color: rgb(255,136,136); + --column_type-string--color: rgb(255, 136, 136); --psp-text-field--border-color: @lightblue600; --button-hover--color: @lightblue600; @@ -36,7 +36,7 @@ perspective-viewer { --hypergrid-header--color: #eee; --hypergrid-positive--color: @lightblue600; - --hypergrid-negative--color: rgb(255,136,136); + --hypergrid-negative--color: rgb(255, 136, 136); --hypergrid-positive--background: @coolgrey700; --hypergrid-negative--background: @coolgrey700; @@ -55,7 +55,8 @@ perspective-viewer { --highcharts-grid-line--stroke: @coolgrey600; --highcharts-tooltip--background: @coolgrey800; - --d3fc-legend-text: rgb(187, 187, 187); + --d3fc-legend--text: rgb(187, 187, 187); + --d3fc-sunburst--labels: rgb(187, 187, 187); --d3fc-axis--ticks: rgb(187, 187, 187); --d3fc-axis--lines: rgb(85, 85, 85); --d3fc-tooltip--background: #333333; @@ -92,7 +93,6 @@ perspective-viewer { #4d342f 70%, #222222 100% ); - --highcharts-heatmap-gradient-full: linear-gradient( #feeb65 0%, #e4521b 22.5%, @@ -102,20 +102,17 @@ perspective-viewer { #42b3d5 67.5%, #dcedc8 100% ); - --highcharts-heatmap-gradient-positive: linear-gradient( #222222 0%, #1a237e 35%, #42b3d5 70%, #dcedc8 100% ); - --highcharts-heatmap-gradient-negative: linear-gradient( #feeb65 0%, #e4521b 35%, #4d342f 70%, #222222 100% ); - --warning--color: #333; } From acf8180f6deaadd85fb738027165f94d72dad13e Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 15:13:03 +0100 Subject: [PATCH 29/47] Moved some data modification into treeData --- .../src/js/charts/sunburst.js | 11 ----------- .../src/js/data/treeData.js | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 88a1903287..5f195b3038 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -60,17 +60,6 @@ function sunburst(container, settings) { title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); const radius = (Math.min(width, height) - 100) / 6; - data.each(d => { - d.current = d; - d.mainValues = settings.mainValues.length === 1 ? d.value : [d.value, d.data.color]; - d.crossValue = d - .ancestors() - .slice(0, -1) - .reverse() - .map(cross => cross.data.name) - .join("|"); - d.key = split; - }); const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); const segmentEnter = segment diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index abf320454b..66acdcf57f 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -46,7 +46,20 @@ export function treeData(settings) { const tree = {name: "root", children: set[1]}; const root = d3.hierarchy(tree).sum(d => d.size); const color = treeColor(settings, set); - return {split: set[0], data: d3.partition().size([2 * Math.PI, root.height + 1])(root), color}; + const chartData = d3.partition().size([2 * Math.PI, root.height + 1])(root); + chartData.each(d => { + d.current = d; + d.mainValues = settings.mainValues.length === 1 ? d.value : [d.value, d.data.color]; + d.crossValue = d + .ancestors() + .slice(0, -1) + .reverse() + .map(cross => cross.data.name) + .join("|"); + d.key = set[0]; + }); + + return {split: set[0], data: chartData, color}; }); return data; From b73906120612bdb10ac080b04358613f3616a7fe Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 15:21:42 +0100 Subject: [PATCH 30/47] Created sunburstSeries file --- .../src/js/axis/sunburstLabel.js | 32 ---- .../src/js/charts/sunburst.js | 53 +------ .../src/js/interaction/clickHandler.js | 56 ------- .../src/js/series/arcSeries.js | 21 --- .../src/js/series/sunburstSeries.js | 137 ++++++++++++++++++ 5 files changed, 139 insertions(+), 160 deletions(-) delete mode 100644 packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js delete mode 100644 packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js delete mode 100644 packages/perspective-viewer-d3fc/src/js/series/arcSeries.js create mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js diff --git a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js deleted file mode 100644 index f9472c3063..0000000000 --- a/packages/perspective-viewer-d3fc/src/js/axis/sunburstLabel.js +++ /dev/null @@ -1,32 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {select} from "d3"; - -export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; - -export function labelTransform(d, radius) { - const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; - const y = ((d.y0 + d.y1) / 2) * radius; - return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; -} - -export function cropLabel(d, targetWidth) { - let actualWidth = this.getBBox().width; - if (actualWidth > targetWidth) { - let labelText = d.data.name; - const textSelection = select(this); - while (actualWidth > targetWidth) { - labelText = labelText.substring(0, labelText.length - 1); - textSelection.text(() => labelText); - actualWidth = this.getBBox().width; - } - textSelection.text(() => `${labelText.substring(0, labelText.length - 3).replace(/\s+$/, "")}...`); - } -} diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 5f195b3038..2b52ad0a8e 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -9,9 +9,7 @@ import {select} from "d3"; import {treeData} from "../data/treeData"; -import {clickHandler} from "../interaction/clickHandler"; -import {drawArc, arcVisible} from "../series/arcSeries"; -import {labelVisible, labelTransform, cropLabel} from "../axis/sunburstLabel"; +import {sunburstSeries} from "../series/sunburstSeries"; import {colorRangeLegend} from "../legend/colorRangeLegend"; import {tooltip} from "../tooltip/tooltip"; @@ -60,54 +58,7 @@ function sunburst(container, settings) { title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); const radius = (Math.min(width, height) - 100) / 6; - - const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); - const segmentEnter = segment - .enter() - .append("g") - .attr("class", "segment"); - - segmentEnter.append("path"); - segmentEnter - .append("text") - .attr("class", "segment") - .attr("dy", "0.35em"); - const segmentMerge = segmentEnter.merge(segment); - - const path = segmentMerge - .select("path") - .attr("fill", d => color(d.data.color)) - .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) - .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) - .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) - .attr("d", d => drawArc(radius)(d.current)); - - const label = segmentMerge - .select("text") - .attr("fill-opacity", d => +labelVisible(d.current)) - .attr("transform", d => labelTransform(d.current, radius)) - .text(d => d.data.name) - .each(function(d) { - cropLabel.call(this, d, radius); - }); - - const parentTitle = sunburstElement.select("text.parent"); - const parent = sunburstElement - .select("circle") - .attr("r", radius) - .datum(data); - - const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius, split, settings); - if (settings.sunburstLevel) { - const currentLevel = data.descendants().find(d => d.data.name === settings.sunburstLevel[split]); - currentLevel && onClick(currentLevel, true); - } else { - settings.sunburstLevel = {}; - } - parent.on("click", d => onClick(d, false)); - path.filter(d => d.children) - .style("cursor", "pointer") - .on("click", d => onClick(d, false)); + sunburstSeries(sunburstElement, settings, split, data, color, radius); const legend = colorRangeLegend().scale(color); select(svgNode.parentNode) diff --git a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js b/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js deleted file mode 100644 index 2f205cad15..0000000000 --- a/packages/perspective-viewer-d3fc/src/js/interaction/clickHandler.js +++ /dev/null @@ -1,56 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {interpolate} from "d3"; -import {drawArc, arcVisible} from "../series/arcSeries"; -import {labelVisible, labelTransform} from "../axis/sunburstLabel"; - -export const clickHandler = (data, g, parent, parentTitle, path, label, radius, split, settings) => (p, skipTransition) => { - settings.sunburstLevel[split] = p.data.name; - if (p.parent) { - parent.datum(p.parent); - parent.style("cursor", "pointer"); - parentTitle.html(`⇪ ${p.parent.data.name}`); - } else { - parent.datum(data); - parent.style("cursor", "default"); - parentTitle.html(""); - } - data.each( - d => - (d.target = { - x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, - x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, - y0: Math.max(0, d.y0 - p.depth), - y1: Math.max(0, d.y1 - p.depth) - }) - ); - - const t = g.transition().duration(skipTransition ? 0 : 750); - path.transition(t) - .tween("data", d => { - const i = interpolate(d.current, d.target); - return t => (d.current = i(t)); - }) - .filter(function(d) { - return +this.getAttribute("fill-opacity") || arcVisible(d.target); - }) - .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) - .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) - .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) - .attrTween("d", d => () => drawArc(radius)(d.current)); - - label - .filter(function(d) { - return +this.getAttribute("fill-opacity") || labelVisible(d.target); - }) - .transition(t) - .attr("fill-opacity", d => +labelVisible(d.target)) - .attrTween("transform", d => () => labelTransform(d.current, radius)); -}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js b/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js deleted file mode 100644 index a22c841cc0..0000000000 --- a/packages/perspective-viewer-d3fc/src/js/series/arcSeries.js +++ /dev/null @@ -1,21 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {arc} from "d3"; - -export const drawArc = radius => - arc() - .startAngle(d => d.x0) - .endAngle(d => d.x1) - .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) - .padRadius(radius) - .innerRadius(d => d.y0 * radius) - .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)); - -export const arcVisible = d => d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js new file mode 100644 index 0000000000..29c9bbc746 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js @@ -0,0 +1,137 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {select, arc, interpolate} from "d3"; + +export function sunburstSeries(sunburstElement, settings, split, data, color, radius) { + const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); + const segmentEnter = segment + .enter() + .append("g") + .attr("class", "segment"); + + segmentEnter.append("path"); + segmentEnter + .append("text") + .attr("class", "segment") + .attr("dy", "0.35em"); + const segmentMerge = segmentEnter.merge(segment); + + const path = segmentMerge + .select("path") + .attr("fill", d => color(d.data.color)) + .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) + .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) + .attr("d", d => drawArc(radius)(d.current)); + + const label = segmentMerge + .select("text") + .attr("fill-opacity", d => +labelVisible(d.current)) + .attr("transform", d => labelTransform(d.current, radius)) + .text(d => d.data.name) + .each(function(d) { + cropLabel.call(this, d, radius); + }); + + const parentTitle = sunburstElement.select("text.parent"); + const parent = sunburstElement + .select("circle") + .attr("r", radius) + .datum(data); + + const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius, split, settings); + if (settings.sunburstLevel) { + const currentLevel = data.descendants().find(d => d.data.name === settings.sunburstLevel[split]); + currentLevel && onClick(currentLevel, true); + } else { + settings.sunburstLevel = {}; + } + parent.on("click", d => onClick(d, false)); + path.filter(d => d.children) + .style("cursor", "pointer") + .on("click", d => onClick(d, false)); +} + +const drawArc = radius => + arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) + .padRadius(radius) + .innerRadius(d => d.y0 * radius) + .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)); + +const arcVisible = d => d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; + +const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; + +function labelTransform(d, radius) { + const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; + const y = ((d.y0 + d.y1) / 2) * radius; + return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; +} + +function cropLabel(d, targetWidth) { + let actualWidth = this.getBBox().width; + if (actualWidth > targetWidth) { + let labelText = d.data.name; + const textSelection = select(this); + while (actualWidth > targetWidth) { + labelText = labelText.substring(0, labelText.length - 1); + textSelection.text(() => labelText); + actualWidth = this.getBBox().width; + } + textSelection.text(() => `${labelText.substring(0, labelText.length - 3).replace(/\s+$/, "")}...`); + } +} + +const clickHandler = (data, g, parent, parentTitle, path, label, radius, split, settings) => (p, skipTransition) => { + settings.sunburstLevel[split] = p.data.name; + if (p.parent) { + parent.datum(p.parent); + parent.style("cursor", "pointer"); + parentTitle.html(`⇪ ${p.parent.data.name}`); + } else { + parent.datum(data); + parent.style("cursor", "default"); + parentTitle.html(""); + } + data.each( + d => + (d.target = { + x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + y0: Math.max(0, d.y0 - p.depth), + y1: Math.max(0, d.y1 - p.depth) + }) + ); + + const t = g.transition().duration(skipTransition ? 0 : 750); + path.transition(t) + .tween("data", d => { + const i = interpolate(d.current, d.target); + return t => (d.current = i(t)); + }) + .filter(function(d) { + return +this.getAttribute("fill-opacity") || arcVisible(d.target); + }) + .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) + .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) + .attrTween("d", d => () => drawArc(radius)(d.current)); + + label + .filter(function(d) { + return +this.getAttribute("fill-opacity") || labelVisible(d.target); + }) + .transition(t) + .attr("fill-opacity", d => +labelVisible(d.target)) + .attrTween("transform", d => () => labelTransform(d.current, radius)); +}; From d5a2fe73ccc3b20a91a29be69aa035231551c179 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 15:33:03 +0100 Subject: [PATCH 31/47] Use css for default fill color --- .../perspective-viewer-d3fc/src/js/charts/sunburst.js | 10 ++++++---- .../perspective-viewer-d3fc/src/js/data/treeData.js | 1 - .../src/js/series/sunburstSeries.js | 4 ++-- packages/perspective-viewer-d3fc/src/less/chart.less | 4 ++++ 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 2b52ad0a8e..2a3f5d2210 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -60,10 +60,12 @@ function sunburst(container, settings) { const radius = (Math.min(width, height) - 100) / 6; sunburstSeries(sunburstElement, settings, split, data, color, radius); - const legend = colorRangeLegend().scale(color); - select(svgNode.parentNode) - .call(legend) - .select("div.legend-container"); + if (color) { + const legend = colorRangeLegend().scale(color); + select(svgNode.parentNode) + .call(legend) + .select("div.legend-container"); + } tooltip().settings(settings)(sunburstElement.selectAll("g.segment")); }); diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index 66acdcf57f..93b71f571b 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -89,5 +89,4 @@ function treeColor(settings, [split, data]) { const max = Math.max(...data.map(d => d.color)); return seriesColorRange(settings, null, null, [min, max]); } - return () => "rgb(31, 119, 180)"; } diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js index 29c9bbc746..eab0518413 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js @@ -25,11 +25,11 @@ export function sunburstSeries(sunburstElement, settings, split, data, color, ra const path = segmentMerge .select("path") - .attr("fill", d => color(d.data.color)) - .attr("fill-opacity", d => (arcVisible(d.current) ? 0.8 : 0)) + .attr("fill-opacity", d => (arcVisible(d.current) ? 1 : 0)) .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) .attr("d", d => drawArc(radius)(d.current)); + color && path.style("fill", d => color(d.data.color)); const label = segmentMerge .select("text") diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index ecbcd89089..c7c55afbd3 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -37,6 +37,10 @@ height: 100%; } + & path { + fill: var(--d3fc-series, rgba(31, 119, 180, 0.5)); + } + & text.title, & text.segment, & text.parent { From a7f2cd8d3f98fcd5f4c7c3bd8f563f8bc70c9379 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 16:30:39 +0100 Subject: [PATCH 32/47] Moved color function to sunburstSeries --- .../src/js/charts/sunburst.js | 5 +++-- .../src/js/data/treeData.js | 14 ++------------ .../src/js/series/sunburstSeries.js | 10 ++++++++++ 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 2a3f5d2210..2b28ac1a30 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -9,7 +9,7 @@ import {select} from "d3"; import {treeData} from "../data/treeData"; -import {sunburstSeries} from "../series/sunburstSeries"; +import {sunburstSeries, treeColor} from "../series/sunburstSeries"; import {colorRangeLegend} from "../legend/colorRangeLegend"; import {tooltip} from "../tooltip/tooltip"; @@ -49,7 +49,7 @@ function sunburst(container, settings) { .select("svg") .select("g.sunburst") .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) - .each(function({split, data, color}) { + .each(function({split, data}) { const sunburstElement = select(this); const svgNode = this.parentNode; const {width, height} = svgNode.getBoundingClientRect(); @@ -58,6 +58,7 @@ function sunburst(container, settings) { title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); const radius = (Math.min(width, height) - 100) / 6; + const color = treeColor(settings, split, data.data.children); sunburstSeries(sunburstElement, settings, split, data, color, radius); if (color) { diff --git a/packages/perspective-viewer-d3fc/src/js/data/treeData.js b/packages/perspective-viewer-d3fc/src/js/data/treeData.js index 93b71f571b..fae2b99ba9 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/treeData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/treeData.js @@ -8,7 +8,6 @@ */ import * as d3 from "d3"; -import {seriesColorRange} from "../series/seriesRange"; export function treeData(settings) { const sets = {}; @@ -45,7 +44,6 @@ export function treeData(settings) { const data = Object.entries(sets).map(set => { const tree = {name: "root", children: set[1]}; const root = d3.hierarchy(tree).sum(d => d.size); - const color = treeColor(settings, set); const chartData = d3.partition().size([2 * Math.PI, root.height + 1])(root); chartData.each(d => { d.current = d; @@ -59,13 +57,13 @@ export function treeData(settings) { d.key = set[0]; }); - return {split: set[0], data: chartData, color}; + return {split: set[0], data: chartData}; }); return data; } -const getDataValue = (d, aggregate, split) => (split.length ? d[`${split}|${aggregate.name}`] : d[aggregate.name]); +export const getDataValue = (d, aggregate, split) => (split.length ? d[`${split}|${aggregate.name}`] : d[aggregate.name]); function getSplitNames(d) { const splits = []; @@ -82,11 +80,3 @@ function getSplitNames(d) { }); return splits; } - -function treeColor(settings, [split, data]) { - if (settings.mainValues.length > 1) { - const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); - const max = Math.max(...data.map(d => d.color)); - return seriesColorRange(settings, null, null, [min, max]); - } -} diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js index eab0518413..8069716491 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js @@ -8,6 +8,8 @@ */ import {select, arc, interpolate} from "d3"; +import {getDataValue} from "../data/treeData"; +import {seriesColorRange} from "../series/seriesRange"; export function sunburstSeries(sunburstElement, settings, split, data, color, radius) { const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); @@ -59,6 +61,14 @@ export function sunburstSeries(sunburstElement, settings, split, data, color, ra .on("click", d => onClick(d, false)); } +export function treeColor(settings, split, data) { + if (settings.mainValues.length > 1) { + const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); + const max = Math.max(...data.map(d => d.color)); + return seriesColorRange(settings, null, null, [min, max]); + } +} + const drawArc = radius => arc() .startAngle(d => d.x0) From 1c718ac5ea597f197b089b07e4649547cc13af2e Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Mon, 1 Apr 2019 16:48:04 +0100 Subject: [PATCH 33/47] Made sunburstSeries more component-like --- .../src/js/charts/sunburst.js | 9 +- .../src/js/series/sunburst/sunburstArc.js | 21 +++ .../src/js/series/sunburst/sunburstClick.js | 56 +++++++ .../src/js/series/sunburst/sunburstLabel.js | 32 ++++ .../src/js/series/sunburst/sunburstSeries.js | 122 +++++++++++++++ .../src/js/series/sunburstSeries.js | 147 ------------------ 6 files changed, 238 insertions(+), 149 deletions(-) create mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstArc.js create mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js create mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstLabel.js create mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js delete mode 100644 packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 2b28ac1a30..1c67517ec7 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -9,7 +9,7 @@ import {select} from "d3"; import {treeData} from "../data/treeData"; -import {sunburstSeries, treeColor} from "../series/sunburstSeries"; +import {sunburstSeries, treeColor} from "../series/sunburst/sunburstSeries"; import {colorRangeLegend} from "../legend/colorRangeLegend"; import {tooltip} from "../tooltip/tooltip"; @@ -59,7 +59,12 @@ function sunburst(container, settings) { const radius = (Math.min(width, height) - 100) / 6; const color = treeColor(settings, split, data.data.children); - sunburstSeries(sunburstElement, settings, split, data, color, radius); + sunburstSeries() + .settings(settings) + .split(split) + .data(data) + .color(color) + .radius(radius)(sunburstElement); if (color) { const legend = colorRangeLegend().scale(color); diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstArc.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstArc.js new file mode 100644 index 0000000000..a22c841cc0 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstArc.js @@ -0,0 +1,21 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {arc} from "d3"; + +export const drawArc = radius => + arc() + .startAngle(d => d.x0) + .endAngle(d => d.x1) + .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) + .padRadius(radius) + .innerRadius(d => d.y0 * radius) + .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)); + +export const arcVisible = d => d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js new file mode 100644 index 0000000000..2202f10ac4 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js @@ -0,0 +1,56 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {interpolate} from "d3"; +import {drawArc, arcVisible} from "./sunburstArc"; +import {labelVisible, labelTransform} from "./sunburstLabel"; + +export const clickHandler = (data, g, parent, parentTitle, path, label, radius, split, settings) => (p, skipTransition) => { + settings.sunburstLevel[split] = p.data.name; + if (p.parent) { + parent.datum(p.parent); + parent.style("cursor", "pointer"); + parentTitle.html(`⇪ ${p.parent.data.name}`); + } else { + parent.datum(data); + parent.style("cursor", "default"); + parentTitle.html(""); + } + data.each( + d => + (d.target = { + x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, + y0: Math.max(0, d.y0 - p.depth), + y1: Math.max(0, d.y1 - p.depth) + }) + ); + + const t = g.transition().duration(skipTransition ? 0 : 750); + path.transition(t) + .tween("data", d => { + const i = interpolate(d.current, d.target); + return t => (d.current = i(t)); + }) + .filter(function(d) { + return +this.getAttribute("fill-opacity") || arcVisible(d.target); + }) + .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) + .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) + .attrTween("d", d => () => drawArc(radius)(d.current)); + + label + .filter(function(d) { + return +this.getAttribute("fill-opacity") || labelVisible(d.target); + }) + .transition(t) + .attr("fill-opacity", d => +labelVisible(d.target)) + .attrTween("transform", d => () => labelTransform(d.current, radius)); +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstLabel.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstLabel.js new file mode 100644 index 0000000000..f9472c3063 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstLabel.js @@ -0,0 +1,32 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {select} from "d3"; + +export const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; + +export function labelTransform(d, radius) { + const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; + const y = ((d.y0 + d.y1) / 2) * radius; + return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; +} + +export function cropLabel(d, targetWidth) { + let actualWidth = this.getBBox().width; + if (actualWidth > targetWidth) { + let labelText = d.data.name; + const textSelection = select(this); + while (actualWidth > targetWidth) { + labelText = labelText.substring(0, labelText.length - 1); + textSelection.text(() => labelText); + actualWidth = this.getBBox().width; + } + textSelection.text(() => `${labelText.substring(0, labelText.length - 3).replace(/\s+$/, "")}...`); + } +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js new file mode 100644 index 0000000000..3af4b7750b --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstSeries.js @@ -0,0 +1,122 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +import {getDataValue} from "../../data/treeData"; +import {seriesColorRange} from "../../series/seriesRange"; +import {drawArc, arcVisible} from "./sunburstArc"; +import {labelVisible, labelTransform, cropLabel} from "./sunburstLabel"; +import {clickHandler} from "./sunburstClick"; + +export function sunburstSeries() { + let settings = null; + let split = null; + let data = null; + let color = null; + let radius = null; + + const _sunburstSeries = sunburstElement => { + const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); + const segmentEnter = segment + .enter() + .append("g") + .attr("class", "segment"); + + segmentEnter.append("path"); + segmentEnter + .append("text") + .attr("class", "segment") + .attr("dy", "0.35em"); + const segmentMerge = segmentEnter.merge(segment); + + const path = segmentMerge + .select("path") + .attr("fill-opacity", d => (arcVisible(d.current) ? 1 : 0)) + .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) + .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) + .attr("d", d => drawArc(radius)(d.current)); + color && path.style("fill", d => color(d.data.color)); + + const label = segmentMerge + .select("text") + .attr("fill-opacity", d => +labelVisible(d.current)) + .attr("transform", d => labelTransform(d.current, radius)) + .text(d => d.data.name) + .each(function(d) { + cropLabel.call(this, d, radius); + }); + + const parentTitle = sunburstElement.select("text.parent"); + const parent = sunburstElement + .select("circle") + .attr("r", radius) + .datum(data); + + const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius, split, settings); + if (settings.sunburstLevel) { + const currentLevel = data.descendants().find(d => d.data.name === settings.sunburstLevel[split]); + currentLevel && onClick(currentLevel, true); + } else { + settings.sunburstLevel = {}; + } + parent.on("click", d => onClick(d, false)); + path.filter(d => d.children) + .style("cursor", "pointer") + .on("click", d => onClick(d, false)); + }; + + _sunburstSeries.settings = (...args) => { + if (!args.length) { + return settings; + } + settings = args[0]; + return _sunburstSeries; + }; + + _sunburstSeries.split = (...args) => { + if (!args.length) { + return split; + } + split = args[0]; + return _sunburstSeries; + }; + + _sunburstSeries.data = (...args) => { + if (!args.length) { + return data; + } + data = args[0]; + return _sunburstSeries; + }; + + _sunburstSeries.color = (...args) => { + if (!args.length) { + return color; + } + color = args[0]; + return _sunburstSeries; + }; + + _sunburstSeries.radius = (...args) => { + if (!args.length) { + return radius; + } + radius = args[0]; + return _sunburstSeries; + }; + + return _sunburstSeries; +} + +export function treeColor(settings, split, data) { + if (settings.mainValues.length > 1) { + const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); + const max = Math.max(...data.map(d => d.color)); + return seriesColorRange(settings, null, null, [min, max]); + } +} diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js b/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js deleted file mode 100644 index 8069716491..0000000000 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburstSeries.js +++ /dev/null @@ -1,147 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ - -import {select, arc, interpolate} from "d3"; -import {getDataValue} from "../data/treeData"; -import {seriesColorRange} from "../series/seriesRange"; - -export function sunburstSeries(sunburstElement, settings, split, data, color, radius) { - const segment = sunburstElement.selectAll("g.segment").data(data.descendants().slice(1)); - const segmentEnter = segment - .enter() - .append("g") - .attr("class", "segment"); - - segmentEnter.append("path"); - segmentEnter - .append("text") - .attr("class", "segment") - .attr("dy", "0.35em"); - const segmentMerge = segmentEnter.merge(segment); - - const path = segmentMerge - .select("path") - .attr("fill-opacity", d => (arcVisible(d.current) ? 1 : 0)) - .attr("user-select", d => (arcVisible(d.current) ? "initial" : "none")) - .attr("pointer-events", d => (arcVisible(d.current) ? "initial" : "none")) - .attr("d", d => drawArc(radius)(d.current)); - color && path.style("fill", d => color(d.data.color)); - - const label = segmentMerge - .select("text") - .attr("fill-opacity", d => +labelVisible(d.current)) - .attr("transform", d => labelTransform(d.current, radius)) - .text(d => d.data.name) - .each(function(d) { - cropLabel.call(this, d, radius); - }); - - const parentTitle = sunburstElement.select("text.parent"); - const parent = sunburstElement - .select("circle") - .attr("r", radius) - .datum(data); - - const onClick = clickHandler(data, sunburstElement, parent, parentTitle, path, label, radius, split, settings); - if (settings.sunburstLevel) { - const currentLevel = data.descendants().find(d => d.data.name === settings.sunburstLevel[split]); - currentLevel && onClick(currentLevel, true); - } else { - settings.sunburstLevel = {}; - } - parent.on("click", d => onClick(d, false)); - path.filter(d => d.children) - .style("cursor", "pointer") - .on("click", d => onClick(d, false)); -} - -export function treeColor(settings, split, data) { - if (settings.mainValues.length > 1) { - const min = Math.min(...settings.data.map(d => getDataValue(d, settings.mainValues[1], split))); - const max = Math.max(...data.map(d => d.color)); - return seriesColorRange(settings, null, null, [min, max]); - } -} - -const drawArc = radius => - arc() - .startAngle(d => d.x0) - .endAngle(d => d.x1) - .padAngle(d => Math.min((d.x1 - d.x0) / 2, 0.005)) - .padRadius(radius) - .innerRadius(d => d.y0 * radius) - .outerRadius(d => Math.max(d.y0 * radius, d.y1 * radius - 1)); - -const arcVisible = d => d.y1 <= 3 && d.y0 >= 1 && d.x1 > d.x0; - -const labelVisible = d => d.y1 <= 3 && d.y0 >= 1 && (d.y1 - d.y0) * (d.x1 - d.x0) > 0.06; - -function labelTransform(d, radius) { - const x = (((d.x0 + d.x1) / 2) * 180) / Math.PI; - const y = ((d.y0 + d.y1) / 2) * radius; - return `rotate(${x - 90}) translate(${y},0) rotate(${x < 180 ? 0 : 180})`; -} - -function cropLabel(d, targetWidth) { - let actualWidth = this.getBBox().width; - if (actualWidth > targetWidth) { - let labelText = d.data.name; - const textSelection = select(this); - while (actualWidth > targetWidth) { - labelText = labelText.substring(0, labelText.length - 1); - textSelection.text(() => labelText); - actualWidth = this.getBBox().width; - } - textSelection.text(() => `${labelText.substring(0, labelText.length - 3).replace(/\s+$/, "")}...`); - } -} - -const clickHandler = (data, g, parent, parentTitle, path, label, radius, split, settings) => (p, skipTransition) => { - settings.sunburstLevel[split] = p.data.name; - if (p.parent) { - parent.datum(p.parent); - parent.style("cursor", "pointer"); - parentTitle.html(`⇪ ${p.parent.data.name}`); - } else { - parent.datum(data); - parent.style("cursor", "default"); - parentTitle.html(""); - } - data.each( - d => - (d.target = { - x0: Math.max(0, Math.min(1, (d.x0 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, - x1: Math.max(0, Math.min(1, (d.x1 - p.x0) / (p.x1 - p.x0))) * 2 * Math.PI, - y0: Math.max(0, d.y0 - p.depth), - y1: Math.max(0, d.y1 - p.depth) - }) - ); - - const t = g.transition().duration(skipTransition ? 0 : 750); - path.transition(t) - .tween("data", d => { - const i = interpolate(d.current, d.target); - return t => (d.current = i(t)); - }) - .filter(function(d) { - return +this.getAttribute("fill-opacity") || arcVisible(d.target); - }) - .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) - .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) - .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) - .attrTween("d", d => () => drawArc(radius)(d.current)); - - label - .filter(function(d) { - return +this.getAttribute("fill-opacity") || labelVisible(d.target); - }) - .transition(t) - .attr("fill-opacity", d => +labelVisible(d.target)) - .attrTween("transform", d => () => labelTransform(d.current, radius)); -}; From 51cad54b7c014cf83ec097abda873977ba95e9af Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 2 Apr 2019 09:41:57 +0100 Subject: [PATCH 34/47] Refactor axis to use common implementation for x and y (#140) * Refactor axis to use a common implementation for x and y Refactor to group by type of axis (linear/time/ordinal) rather than by direction. Use a common implementation for each chart for creating an axis, based on the settings and data it will be working from. Allow for aggregates that are strings turned into numbers (e.g. `count`) or left as string (e.g. `dominant`) * Allow charts to say which axis types they can't use * Applied new axis types to other charts * Rename files * Refactor `labelFunction` and remove `crossAxis` * Fix variances in screenshot tests * Restore heatmap vertical gridline * Exclude "linear" ranges from various charts * Default padding for chart axis, which can be overridden * Restore label rotation/hiding code that was lost in merge --- .../src/js/axis/axisFactory.js | 134 +++++++++++++ .../src/js/axis/axisLabel.js | 42 ++++ .../src/js/axis/axisType.js | 73 +++++++ .../src/js/axis/chartFactory.js | 38 ++++ .../src/js/axis/flatten.js | 25 +++ .../src/js/axis/linearAxis.js | 70 +++++++ .../src/js/axis/mainAxis.js | 69 ------- .../src/js/axis/noAxis.js | 53 +++++ .../js/axis/{crossAxis.js => ordinalAxis.js} | 189 ++++++------------ .../src/js/axis/timeAxis.js | 63 ++++++ .../src/js/charts/area.js | 51 ++--- .../src/js/charts/bar.js | 48 ++--- .../src/js/charts/column.js | 43 ++-- .../src/js/charts/heatmap.js | 49 ++--- .../src/js/charts/line.js | 44 ++-- .../src/js/charts/ohlcCandle.js | 60 ++---- .../src/js/charts/xy-scatter.js | 37 ++-- .../src/js/charts/y-scatter.js | 43 ++-- .../src/js/data/groupData.js | 2 +- .../src/js/data/heatmapData.js | 2 +- .../src/js/data/ohlcData.js | 2 +- .../src/js/data/pointData.js | 2 +- .../src/js/data/splitAndBaseData.js | 2 +- .../src/js/data/splitData.js | 2 +- .../src/js/plugin/plugin.js | 18 +- .../src/js/series/barSeries.js | 20 +- .../src/js/series/seriesRange.js | 2 +- 27 files changed, 743 insertions(+), 440 deletions(-) create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/axisType.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/flatten.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js delete mode 100644 packages/perspective-viewer-d3fc/src/js/axis/mainAxis.js create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/noAxis.js rename packages/perspective-viewer-d3fc/src/js/axis/{crossAxis.js => ordinalAxis.js} (53%) create mode 100644 packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js b/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js new file mode 100644 index 0000000000..99878072b8 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisFactory.js @@ -0,0 +1,134 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; +import {axisType} from "./axisType"; +import * as none from "./noAxis"; +import * as linear from "./linearAxis"; +import * as time from "./timeAxis"; +import * as ordinal from "./ordinalAxis"; + +const axisTypes = { + none, + ordinal, + time, + linear +}; + +export const axisFactory = settings => { + let excludeType = null; + let orient = "horizontal"; + let settingName = "crossValues"; + let settingValue = null; + let valueNames = ["crossValue"]; + + const optionalParams = ["include", "paddingStrategy", "pad"]; + const optional = {}; + + const _factory = data => { + const useType = axisType(settings) + .excludeType(excludeType) + .settingName(settingName) + .settingValue(settingValue)(); + + const axis = axisTypes[useType]; + const domainFunction = axis.domain().valueNames(valueNames); + + optionalParams.forEach(p => { + if (optional[p] && domainFunction[p]) domainFunction[p](optional[p]); + }); + if (domainFunction.orient) domainFunction.orient(orient); + + const domain = domainFunction(data); + const component = axis.component ? createComponent(axis, domain, data) : defaultComponent(); + + return { + scale: axis.scale(), + domain, + labelFunction: axis.labelFunction, + component: { + bottom: component.bottom, + left: component.left + }, + size: component.size, + decorate: component.decorate, + label: settings[settingName].map(v => v.name).join(", ") + }; + }; + + const createComponent = (axis, domain, data) => + axis + .component(settings) + .orient(orient) + .settingName(settingName) + .domain(domain)(data); + + const defaultComponent = () => ({ + bottom: fc.axisBottom, + left: fc.axisLeft, + decorate: () => {} + }); + + _factory.excludeType = (...args) => { + if (!args.length) { + return excludeType; + } + excludeType = args[0]; + return _factory; + }; + + _factory.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return _factory; + }; + + _factory.settingName = (...args) => { + if (!args.length) { + return settingName; + } + settingName = args[0]; + return _factory; + }; + _factory.settingValue = (...args) => { + if (!args.length) { + return settingValue; + } + settingValue = args[0]; + return _factory; + }; + + _factory.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _factory; + }; + _factory.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _factory; + }; + + optionalParams.forEach(p => { + _factory[p] = (...args) => { + if (!args.length) { + return optional[p]; + } + optional[p] = args[0]; + return _factory; + }; + }); + + return _factory; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js b/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js new file mode 100644 index 0000000000..97c7b857d6 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisLabel.js @@ -0,0 +1,42 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import {rebindAll} from "d3fc"; +import {axisType} from "./axisType"; +import {labelFunction as noLabel} from "./noAxis"; +import {labelFunction as timeLabel} from "./timeAxis"; +import {labelFunction as linearLabel} from "./linearAxis"; +import {labelFunction as ordinalLabel} from "./ordinalAxis"; + +const labelFunctions = { + none: noLabel, + ordinal: ordinalLabel, + time: timeLabel, + linear: linearLabel +}; + +export const labelFunction = settings => { + const base = axisType(settings); + let valueName = "__ROW_PATH__"; + + const label = (d, i) => { + return labelFunctions[base()](valueName)(d, i); + }; + + rebindAll(label, base); + + label.valueName = (...args) => { + if (!args.length) { + return valueName; + } + valueName = args[0]; + return label; + }; + + return label; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/axisType.js b/packages/perspective-viewer-d3fc/src/js/axis/axisType.js new file mode 100644 index 0000000000..b312a3bb2f --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/axisType.js @@ -0,0 +1,73 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export const AXIS_TYPES = { + none: "none", + ordinal: "ordinal", + time: "time", + linear: "linear" +}; + +export const axisType = settings => { + let settingName = "crossValues"; + let settingValue = null; + let excludeType = null; + + const getType = () => { + const checkTypes = types => { + const list = settingValue ? settings[settingName].filter(s => settingValue == s.name) : settings[settingName]; + + if (settingName == "crossValues" && list.length > 1) { + // can't do multiple values on non-ordinal cross-axis + return false; + } + + return list.some(s => types.includes(s.type)); + }; + + if (settings[settingName].length === 0) { + return AXIS_TYPES.none; + } else if (excludeType != AXIS_TYPES.time && checkTypes(["datetime"])) { + return AXIS_TYPES.time; + } else if (excludeType != AXIS_TYPES.linear && checkTypes(["integer", "float"])) { + return AXIS_TYPES.linear; + } + + if (excludeType == AXIS_TYPES.ordinal) { + return AXIS_TYPES.linear; + } + return AXIS_TYPES.ordinal; + }; + + getType.settingName = (...args) => { + if (!args.length) { + return settingName; + } + settingName = args[0]; + return getType; + }; + + getType.settingValue = (...args) => { + if (!args.length) { + return settingValue; + } + settingValue = args[0]; + return getType; + }; + + getType.excludeType = (...args) => { + if (!args.length) { + return excludeType; + } + excludeType = args[0]; + return getType; + }; + + return getType; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js b/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js new file mode 100644 index 0000000000..cc8c45a254 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/chartFactory.js @@ -0,0 +1,38 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as fc from "d3fc"; + +export const chartSvgFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartSvgCartesian); +export const chartCanvasFactory = (xAxis, yAxis) => chartFactory(xAxis, yAxis, fc.chartCanvasCartesian); + +const chartFactory = (xAxis, yAxis, cartesian) => { + const chart = cartesian({ + xScale: xAxis.scale, + yScale: yAxis.scale, + xAxis: xAxis.component, + yAxis: yAxis.component + }) + .xDomain(xAxis.domain) + .xLabel(xAxis.label) + .xAxisHeight(xAxis.size) + .xDecorate(xAxis.decorate) + .yDomain(yAxis.domain) + .yLabel(yAxis.label) + .yAxisWidth(yAxis.size) + .yDecorate(yAxis.decorate) + .yOrient("left"); + + // Padding defaults can be overridden + chart.xPaddingInner && chart.xPaddingInner(1); + chart.xPaddingOuter && chart.xPaddingOuter(0.5); + chart.yPaddingInner && chart.yPaddingInner(1); + chart.yPaddingOuter && chart.yPaddingOuter(0.5); + + return chart; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/flatten.js b/packages/perspective-viewer-d3fc/src/js/axis/flatten.js new file mode 100644 index 0000000000..88b5084d68 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/flatten.js @@ -0,0 +1,25 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +export const flattenExtent = array => { + const withUndefined = fn => (a, b) => { + if (a === undefined) return b; + if (b === undefined) return a; + return fn(a, b); + }; + return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]); +}; + +export const flattenArray = array => { + if (Array.isArray(array)) { + return [].concat(...array.map(flattenArray)); + } else { + return [array]; + } +}; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js new file mode 100644 index 0000000000..43a0a76665 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/linearAxis.js @@ -0,0 +1,70 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; +import {flattenArray} from "./flatten"; +import {extentLinear as customExtent} from "../d3fc/extent/extentLinear"; + +export const scale = () => d3.scaleLinear(); + +export const domain = () => { + const base = customExtent() + .pad([0, 0.1]) + .padUnit("percent"); + + let valueNames = ["crossValue"]; + + const _domain = data => { + base.accessors(valueNames.map(v => d => parseFloat(d[v]))); + + return getDataExtent(flattenArray(data)); + }; + + fc.rebindAll(_domain, base); + + const getMinimumGap = data => { + const gaps = valueNames.map(valueName => + data + .map(d => d[valueName]) + .sort((a, b) => a - b) + .filter((d, i, a) => i === 0 || d !== a[i - 1]) + .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) + ); + + return Math.min(...gaps); + }; + + const getDataExtent = data => { + if (base.padUnit() == "domain") { + const dataWidth = getMinimumGap(data); + return base.pad([dataWidth / 2, dataWidth / 2])(data); + } else { + return base(data); + } + }; + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => d[valueName][0]; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/mainAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/mainAxis.js deleted file mode 100644 index 4d0090c38d..0000000000 --- a/packages/perspective-viewer-d3fc/src/js/axis/mainAxis.js +++ /dev/null @@ -1,69 +0,0 @@ -/****************************************************************************** - * - * Copyright (c) 2017, the Perspective Authors. - * - * This file is part of the Perspective library, distributed under the terms of - * the Apache License 2.0. The full license can be found in the LICENSE file. - * - */ -import * as d3 from "d3"; -import * as fc from "d3fc"; -import {extentLinear as customExtent} from "../d3fc/extent/extentLinear"; - -export const scale = () => d3.scaleLinear(); - -export const domain = () => { - let valueNames = ["mainValue"]; - - const extentLinear = customExtent() - .pad([0, 0.1]) - .padUnit("percent"); - - const _domain = function(data) { - const extent = getDataExtentFromArray(data, valueNames); - return extentLinear(extent); - }; - - fc.rebindAll(_domain, extentLinear); - - _domain.valueName = (...args) => { - if (!args.length) { - return valueNames[0]; - } - valueNames = [args[0]]; - return _domain; - }; - _domain.valueNames = (...args) => { - if (!args.length) { - return valueNames; - } - valueNames = args[0]; - return _domain; - }; - - return _domain; -}; - -const getDataExtentFromArray = (array, valueNames) => { - const dataExtent = array.map(v => getDataExtentFromValue(v, valueNames)); - const extent = flattenExtent(dataExtent); - return extent; -}; - -const getDataExtentFromValue = (value, valueNames) => { - if (Array.isArray(value)) { - return getDataExtentFromArray(value, valueNames); - } - return [Math.min(...valueNames.map(n => value[n])), Math.max(...valueNames.map(n => value[n]))]; -}; - -export const label = settings => settings.mainValues.map(v => v.name).join(", "); - -function flattenExtent(array) { - const withUndefined = fn => (a, b) => { - if (a === undefined) return b; - if (b === undefined) return a; - return fn(a, b); - }; - return array.reduce((r, v) => [withUndefined(Math.min)(r[0], v[0]), withUndefined(Math.max)(r[1], v[1])], [undefined, undefined]); -} diff --git a/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js new file mode 100644 index 0000000000..3dadf13d7d --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/noAxis.js @@ -0,0 +1,53 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import {flattenArray} from "./flatten"; +import minBandwidth from "./minBandwidth"; +import withoutTicks from "./withoutTicks"; + +export const scale = () => withoutTicks(minBandwidth(d3.scaleBand())); + +export const domain = () => { + let valueNames = ["crossValue"]; + let orient = "horizontal"; + + const _domain = data => { + const flattenedData = flattenArray(data); + return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); + }; + + const transformDomain = d => (orient == "vertical" ? d.reverse() : d); + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + _domain.orient = (...args) => { + if (!args.length) { + return orient; + } + orient = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => d[valueName].join("|"); diff --git a/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js similarity index 53% rename from packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js rename to packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js index d8dde4c535..c9e28c9160 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/crossAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js @@ -9,166 +9,89 @@ import * as d3 from "d3"; import * as fc from "d3fc"; import minBandwidth from "./minBandwidth"; -import withoutTicks from "./withoutTicks"; +import {flattenArray} from "./flatten"; import {multiAxisBottom, multiAxisLeft} from "../d3fc/axis/multi-axis"; import {getChartContainer} from "../plugin/root"; -const AXIS_TYPES = { - none: "none", - ordinal: "ordinal", - time: "time", - linear: "linear" -}; - -export const scale = (settings, settingName = "crossValues") => { - switch (axisType(settings, settingName)) { - case AXIS_TYPES.none: - return withoutTicks(defaultScaleBand()); - case AXIS_TYPES.time: - return d3.scaleTime(); - case AXIS_TYPES.linear: - return d3.scaleLinear(); - default: - return defaultScaleBand(); - } -}; - -const defaultScaleBand = () => minBandwidth(d3.scaleBand()); - -const flattenArray = array => { - if (Array.isArray(array)) { - return [].concat(...array.map(flattenArray)); - } else { - return [array]; - } -}; - -const getMinimumGap = (data, dataMap) => - data - .map(dataMap) - .sort((a, b) => a - b) - .filter((d, i, a) => i === 0 || d !== a[i - 1]) - .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : d - src[i - 1])); - -export const domain = settings => { - let valueName = "crossValue"; - let settingName = "crossValues"; +export const scale = () => minBandwidth(d3.scaleBand()).padding(0.5); - const extentTime = fc - .extentTime() - .accessors([d => new Date(d[valueName])]) - .padUnit("domain"); +export const domain = () => { + let valueNames = ["crossValue"]; + let orient = "horizontal"; - const _domain = function(data) { + const _domain = data => { const flattenedData = flattenArray(data); - switch (axisType(settings, settingName)) { - case AXIS_TYPES.time: - const dataWidth = getMinimumGap(flattenedData, d => new Date(d[valueName]).getTime()); - return extentTime.pad([dataWidth / 2, dataWidth / 2])(flattenedData); - default: - return [...new Set(flattenedData.map(d => d[valueName]))]; - } + return transformDomain([...new Set(flattenedData.map(d => d[valueNames[0]]))]); }; - switch (axisType(settings)) { - case AXIS_TYPES.time: - fc.rebindAll(_domain, extentTime); - break; - } + const transformDomain = d => (orient == "vertical" ? d.reverse() : d); _domain.valueName = (...args) => { if (!args.length) { - return valueName; + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; } - valueName = args[0]; + valueNames = args[0]; return _domain; }; - _domain.settingName = (...args) => { + _domain.orient = (...args) => { if (!args.length) { - return settingName; + return orient; } - settingName = args[0]; + orient = args[0]; return _domain; }; return _domain; }; -export const labelFunction = (settings, valueName = "__ROW_PATH__", settingName = "crossValues") => { - switch (axisType(settings, settingName)) { - case AXIS_TYPES.none: - return d => d[valueName][0]; - case AXIS_TYPES.time: - return d => new Date(d[valueName][0]); - case AXIS_TYPES.linear: - return d => d[valueName][0]; - default: - return d => d[valueName].join("|"); - } -}; - -export const label = (settings, settingName = "crossValues") => settings[settingName].map(v => v.name).join(", "); +export const labelFunction = valueName => d => d[valueName].join("|"); -const axisType = (settings, settingName = "crossValues") => { - if (settings[settingName].length === 0) { - return AXIS_TYPES.none; - } else if (settings[settingName].length === 1) { - if (settings[settingName][0].type === "datetime") { - return AXIS_TYPES.time; - } - } - return AXIS_TYPES.ordinal; -}; - -export const axisFactory = settings => { +export const component = settings => { let orient = "horizontal"; let settingName = "crossValues"; let domain = null; - const factory = () => { - switch (axisType(settings, settingName)) { - case AXIS_TYPES.ordinal: - const multiLevel = settings[settingName].length > 1 && settings[settingName].every(v => v.type !== "datetime"); - - // Calculate the label groups and corresponding group sizes - const levelGroups = axisGroups(domain); - const groupTickLayout = levelGroups.map(getGroupTickLayout); + const getComponent = () => { + const multiLevel = settings[settingName].length > 1; - const tickSizeInner = multiLevel ? groupTickLayout.map(l => l.size) : groupTickLayout[0].size; - const tickSizeOuter = groupTickLayout.reduce((s, v) => s + v.size, 0); + // Calculate the label groups and corresponding group sizes + const levelGroups = axisGroups(domain); + const groupTickLayout = levelGroups.map(getGroupTickLayout); - const createAxis = scale => { - const axis = pickAxis(multiLevel)(scale); + const tickSizeInner = multiLevel ? groupTickLayout.map(l => l.size) : groupTickLayout[0].size; + const tickSizeOuter = groupTickLayout.reduce((s, v) => s + v.size, 0); - if (multiLevel) { - axis.groups(levelGroups) - .tickSizeInner(tickSizeInner) - .tickSizeOuter(tickSizeOuter); - } - if (orient !== "horizontal") axis.tickPadding(10); - return axis; - }; + const createAxis = scale => { + const axis = pickAxis(multiLevel)(scale); - const decorate = (s, data, index) => { - const rotation = groupTickLayout[index].rotation; - if (orient === "horizontal") applyLabelRotation(s, rotation); - hideOverlappingLabels(s, rotation); - }; + if (multiLevel) { + axis.groups(levelGroups) + .tickSizeInner(tickSizeInner) + .tickSizeOuter(tickSizeOuter); + } + if (orient !== "horizontal") axis.tickPadding(10); + return axis; + }; - return { - bottom: createAxis, - left: createAxis, - size: `${tickSizeOuter + 10}px`, - decorate - }; - } + const decorate = (s, data, index) => { + const rotation = groupTickLayout[index].rotation; + if (orient === "horizontal") applyLabelRotation(s, rotation); + hideOverlappingLabels(s, rotation); + }; - // Default axis return { - bottom: fc.axisBottom, - left: fc.axisLeft, - decorate: () => {} + bottom: createAxis, + left: createAxis, + size: `${tickSizeOuter + 10}px`, + decorate }; }; @@ -182,7 +105,7 @@ export const axisFactory = settings => { const axisGroups = domain => { const groups = []; domain.forEach(tick => { - const split = tick.split("|"); + const split = tick.split ? tick.split("|") : [tick]; split.forEach((s, i) => { while (groups.length <= i) groups.push([]); @@ -302,29 +225,29 @@ export const axisFactory = settings => { }); }; - factory.orient = (...args) => { + getComponent.orient = (...args) => { if (!args.length) { return orient; } orient = args[0]; - return factory; + return getComponent; }; - factory.settingName = (...args) => { + getComponent.settingName = (...args) => { if (!args.length) { return settingName; } settingName = args[0]; - return factory; + return getComponent; }; - factory.domain = (...args) => { + getComponent.domain = (...args) => { if (!args.length) { return domain; } domain = args[0]; - return factory; + return getComponent; }; - return factory; + return getComponent; }; diff --git a/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js new file mode 100644 index 0000000000..916efc7aa0 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/axis/timeAxis.js @@ -0,0 +1,63 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +import * as d3 from "d3"; +import * as fc from "d3fc"; +import {flattenArray} from "./flatten"; + +export const scale = () => d3.scaleTime(); + +export const domain = () => { + const base = fc.extentTime(); + + let valueNames = ["crossValue"]; + + const _domain = data => { + base.accessors(valueNames.map(v => d => new Date(d[v]))); + + return getDataExtent(flattenArray(data)); + }; + + fc.rebindAll(_domain, base, fc.exclude("include", "paddingStrategy")); + + const getMinimumGap = data => { + const gaps = valueNames.map(valueName => + data + .map(d => new Date(d[valueName]).getTime()) + .sort((a, b) => a - b) + .filter((d, i, a) => i === 0 || d !== a[i - 1]) + .reduce((acc, d, i, src) => (i === 0 || acc <= d - src[i - 1] ? acc : Math.abs(d - src[i - 1]))) + ); + + return Math.min(...gaps); + }; + + const getDataExtent = data => { + const dataWidth = getMinimumGap(data); + return base.padUnit("domain").pad([dataWidth / 2, dataWidth / 2])(data); + }; + + _domain.valueName = (...args) => { + if (!args.length) { + return valueNames[0]; + } + valueNames = [args[0]]; + return _domain; + }; + _domain.valueNames = (...args) => { + if (!args.length) { + return valueNames; + } + valueNames = args[0]; + return _domain; + }; + + return _domain; +}; + +export const labelFunction = valueName => d => new Date(d[valueName][0]); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/area.js b/packages/perspective-viewer-d3fc/src/js/charts/area.js index 66dc30b613..7cc444ed19 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/area.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/area.js @@ -7,8 +7,9 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {AXIS_TYPES} from "../axis/axisType"; import {areaSeries} from "../series/areaSeries"; import {seriesColors} from "../series/seriesColors"; import {splitAndBaseData} from "../data/splitAndBaseData"; @@ -30,45 +31,31 @@ function areaChart(container, settings) { const series = fc.seriesSvgRepeat().series(areaSeries(settings, color).orient("vertical")); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const yScale = mainAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .orient("vertical") + .include([0]) + .paddingStrategy(hardLimitZeroPadding())(data); - const chart = fc - .chartSvgCartesian({ - xScale, - yScale, - xAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain( - mainAxis - .domain(settings) - .include([0]) - .paddingStrategy(hardLimitZeroPadding())(data) - ) - .yLabel(crossAxis.label(settings)) - .yOrient("left") - .yLabel(mainAxis.label(settings)) - .yNice() - .plotArea(withGridLines(series).orient("vertical")); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("vertical")); - chart.xPaddingInner && chart.xPaddingInner(1); - chart.xPaddingOuter && chart.xPaddingOuter(0.5); + chart.yNice && chart.yNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale); + .xScale(xAxis.scale); const toolTip = nearbyTip() .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .color(color) .data(data); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/bar.js b/packages/perspective-viewer-d3fc/src/js/charts/bar.js index f886a4e4c3..0ae303fc7b 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/bar.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/bar.js @@ -7,8 +7,9 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {AXIS_TYPES} from "../axis/axisType"; import {barSeries} from "../series/barSeries"; import {seriesColors} from "../series/seriesColors"; import {groupAndStackData} from "../data/groupData"; @@ -33,46 +34,31 @@ function barChart(container, settings) { .mapping((data, index) => data[index]) .series(data.map(() => bars)); - const yDomain = crossAxis - .domain(settings)(data) - .reverse(); - const yScale = crossAxis.scale(settings); - const yAxis = crossAxis - .axisFactory(settings) - .domain(yDomain) - .orient("vertical")(); + const xAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .include([0]) + .paddingStrategy(hardLimitZeroPadding())(data); + const yAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue") + .orient("vertical")(data); - const chart = fc - .chartSvgCartesian({ - xScale: mainAxis.scale(settings), - yScale, - yAxis - }) - .xDomain( - mainAxis - .domain(settings) - .include([0]) - .paddingStrategy(hardLimitZeroPadding())(data) - ) - .xLabel(mainAxis.label(settings)) - .yDomain(yDomain) - .yLabel(crossAxis.label(settings)) - .yAxisWidth(yAxis.size) - .yDecorate(yAxis.decorate) - .yOrient("left") - .xNice() - .plotArea(withGridLines(series).orient("horizontal")); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("horizontal")); if (chart.yPaddingInner) { chart.yPaddingInner(0.5); chart.yPaddingOuter(0.25); bars.align("left"); } + chart.xNice && chart.xNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .yScale(yScale); + .yScale(yAxis.scale); // render container.datum(data).call(zoomChart); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/column.js b/packages/perspective-viewer-d3fc/src/js/charts/column.js index b51084a1d9..b1203909bb 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/column.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/column.js @@ -7,8 +7,9 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {chartSvgFactory} from "../axis/chartFactory"; +import {AXIS_TYPES} from "../axis/axisType"; import {barSeries} from "../series/barSeries"; import {seriesColors} from "../series/seriesColors"; import {groupAndStackData} from "../data/groupData"; @@ -32,41 +33,31 @@ function columnChart(container, settings) { .mapping((data, index) => data[index]) .series(data.map(() => bars)); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .excludeType(AXIS_TYPES.ordinal) + .orient("vertical") + .include([0]) + .paddingStrategy(hardLimitZeroPadding())(data); - const chart = fc - .chartSvgCartesian({ - xScale, - yScale: mainAxis.scale(settings), - xAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain( - mainAxis - .domain(settings) - .include([0]) - .paddingStrategy(hardLimitZeroPadding())(data) - ) - .yLabel(mainAxis.label(settings)) - .yOrient("left") - .yNice() - .plotArea(withGridLines(series).orient("vertical")); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("vertical")); if (chart.xPaddingInner) { chart.xPaddingInner(0.5); chart.xPaddingOuter(0.25); bars.align("left"); } + chart.yNice && chart.yNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale); + .xScale(xAxis.scale); // render container.datum(data).call(zoomChart); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/heatmap.js b/packages/perspective-viewer-d3fc/src/js/charts/heatmap.js index 603519081b..6d1a433bf5 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/heatmap.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/heatmap.js @@ -6,8 +6,9 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {AXIS_TYPES} from "../axis/axisType"; +import {chartSvgFactory} from "../axis/chartFactory"; import {heatmapSeries} from "../series/heatmapSeries"; import {seriesColorRange} from "../series/seriesRange"; import {heatmapData} from "../data/heatmapData"; @@ -24,39 +25,17 @@ function heatmapChart(container, settings) { const legend = colorRangeLegend().scale(color); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); - - const yDomain = crossAxis - .domain(settings) - .settingName("splitValues") - .valueName("mainValue")(data) - .reverse(); - const yScale = crossAxis.scale(settings, "splitValues"); - const yAxis = crossAxis - .axisFactory(settings) + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) .settingName("splitValues") - .orient("vertical") - .domain(yDomain)(); + .valueName("mainValue") + .orient("vertical")(data); - const chart = fc - .chartSvgCartesian({ - xScale, - yScale, - xAxis, - yAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain(yDomain) - .yLabel(crossAxis.label(settings, "splitValues")) - .yAxisWidth(yAxis.size) - .yDecorate(yAxis.decorate) - .yOrient("left") - .plotArea(withGridLines(series)); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series)); if (chart.xPaddingInner) { chart.xPaddingInner(0); @@ -72,8 +51,8 @@ function heatmapChart(container, settings) { const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale) - .yScale(yScale); + .xScale(xAxis.scale) + .yScale(yAxis.scale); // render container.datum(data).call(zoomChart); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/line.js b/packages/perspective-viewer-d3fc/src/js/charts/line.js index eb2f6fbdb5..faa6023849 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/line.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/line.js @@ -7,8 +7,9 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {AXIS_TYPES} from "../axis/axisType"; +import {chartSvgFactory} from "../axis/chartFactory"; import {seriesColors} from "../series/seriesColors"; import {lineSeries} from "../series/lineSeries"; import {splitData} from "../data/splitData"; @@ -34,39 +35,30 @@ function lineChart(container, settings) { .pad([0.1, 0.1]) .padUnit("percent"); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); - const yScale = mainAxis.scale(settings); + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .orient("vertical") + .include([0]) + .paddingStrategy(paddingStrategy)(data); - const chart = fc - .chartSvgCartesian({ - xScale, - yScale, - xAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain(mainAxis.domain(settings).paddingStrategy(paddingStrategy)(data)) - .yLabel(mainAxis.label(settings)) - .yOrient("left") - .yNice() - .plotArea(withGridLines(series).orient("vertical")); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("vertical")); - chart.xPaddingInner && chart.xPaddingInner(1); - chart.xPaddingOuter && chart.xPaddingOuter(0.5); + chart.yNice && chart.yNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale); + .xScale(xAxis.scale); const toolTip = nearbyTip() .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .color(color) .data(data); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js b/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js index ab06b99df8..63851e6a59 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/ohlcCandle.js @@ -7,8 +7,8 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {chartCanvasFactory} from "../axis/chartFactory"; import {ohlcData} from "../data/ohlcData"; import {filterDataByGroup} from "../legend/filter"; import withGridLines from "../gridlines/gridlines"; @@ -55,50 +55,34 @@ function ohlcCandle(seriesCanvas) { .pad([0.1, 0.1]) .padUnit("percent"); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); - const yScale = mainAxis.scale(settings); - - const chart = fc - .chartCanvasCartesian({ - xScale, - yScale, - xAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain( - mainAxis - .domain(settings) - .valueNames(["lowValue", "highValue"]) - .paddingStrategy(paddingStrategy)(data) - ) - .yLabel(mainAxis.label(settings)) - .yOrient("left") - .yNice() - .plotArea( - withGridLines(multi) - .orient("vertical") - .canvas(true) - ); - - chart.xPaddingInner && chart.xPaddingInner(1); - chart.xPaddingOuter && chart.xPaddingOuter(0.5); + const xAxis = axisFactory(settings) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .settingName("mainValues") + .valueNames(["lowValue", "highValue"]) + .orient("vertical") + .paddingStrategy(paddingStrategy)(data); + + const chart = chartCanvasFactory(xAxis, yAxis).plotArea( + withGridLines(multi) + .orient("vertical") + .canvas(true) + ); + + chart.yNice && chart.yNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .canvas(true); const toolTip = nearbyTip() .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .yValueName("closeValue") .color(upColor) .data(data) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/xy-scatter.js b/packages/perspective-viewer-d3fc/src/js/charts/xy-scatter.js index abac2885cf..d6d7d5a3a1 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/xy-scatter.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/xy-scatter.js @@ -7,7 +7,8 @@ * */ import * as fc from "d3fc"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {chartCanvasFactory} from "../axis/chartFactory"; import {pointSeriesCanvas, symbolTypeFromGroups} from "../series/pointSeriesCanvas"; import {pointData} from "../data/pointData"; import {seriesColorsFromGroups} from "../series/seriesColors"; @@ -46,36 +47,42 @@ function xyScatter(container, settings) { .mapping((data, index) => data[index]) .series(data.map(series => pointSeriesCanvas(settings, series.key, size, color, symbols))); - const domainDefault = () => mainAxis.domain(settings).paddingStrategy(hardLimitZeroPadding().pad([0.1, 0.1])); + const axisDefault = () => + axisFactory(settings) + .settingName("mainValues") + .paddingStrategy(hardLimitZeroPadding()) + .pad([0.1, 0.1]); - const xScale = mainAxis.scale(settings); - const yScale = mainAxis.scale(settings); + const xAxis = axisDefault() + .settingValue(settings.mainValues[0].name) + .valueName("x")(data); + const yAxis = axisDefault() + .orient("vertical") + .settingValue(settings.mainValues[1].name) + .valueName("y")(data); - const chart = fc - .chartCanvasCartesian(xScale, yScale) - .xDomain(domainDefault().valueName("x")(data)) + const chart = chartCanvasFactory(xAxis, yAxis) .xLabel(settings.mainValues[0].name) - .yDomain(domainDefault().valueName("y")(data)) .yLabel(settings.mainValues[1].name) - .yOrient("left") - .yNice() - .xNice() .plotArea(withGridLines(series).canvas(true)); + chart.xNice && chart.xNice(); + chart.yNice && chart.yNice(); + const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .canvas(true); const toolTip = nearbyTip() .settings(settings) .canvas(true) - .xScale(xScale) + .xScale(xAxis.scale) .xValueName("x") .yValueName("y") - .yScale(yScale) + .yScale(yAxis.scale) .color(useGroupColors && color) .size(size) .data(data); diff --git a/packages/perspective-viewer-d3fc/src/js/charts/y-scatter.js b/packages/perspective-viewer-d3fc/src/js/charts/y-scatter.js index c1b062cbef..c98b0e3ed7 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/y-scatter.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/y-scatter.js @@ -7,8 +7,9 @@ * */ import * as fc from "d3fc"; -import * as crossAxis from "../axis/crossAxis"; -import * as mainAxis from "../axis/mainAxis"; +import {axisFactory} from "../axis/axisFactory"; +import {AXIS_TYPES} from "../axis/axisType"; +import {chartSvgFactory} from "../axis/chartFactory"; import {seriesColors} from "../series/seriesColors"; import {categoryPointSeries, symbolType} from "../series/categoryPointSeries"; import {groupData} from "../data/groupData"; @@ -38,39 +39,29 @@ function yScatter(container, settings) { .pad([0.05, 0.05]) .padUnit("percent"); - const xDomain = crossAxis.domain(settings)(data); - const xScale = crossAxis.scale(settings); - const xAxis = crossAxis.axisFactory(settings).domain(xDomain)(); - const yScale = mainAxis.scale(settings); + const xAxis = axisFactory(settings) + .excludeType(AXIS_TYPES.linear) + .settingName("crossValues") + .valueName("crossValue")(data); + const yAxis = axisFactory(settings) + .settingName("mainValues") + .valueName("mainValue") + .orient("vertical") + .paddingStrategy(paddingStrategy)(data); - const chart = fc - .chartSvgCartesian({ - xScale, - yScale, - xAxis - }) - .xDomain(xDomain) - .xLabel(crossAxis.label(settings)) - .xAxisHeight(xAxis.size) - .xDecorate(xAxis.decorate) - .yDomain(mainAxis.domain(settings).paddingStrategy(paddingStrategy)(data)) - .yLabel(mainAxis.label(settings)) - .yOrient("left") - .yNice() - .plotArea(withGridLines(series).orient("vertical")); + const chart = chartSvgFactory(xAxis, yAxis).plotArea(withGridLines(series).orient("vertical")); - chart.xPaddingInner && chart.xPaddingInner(1); - chart.xPaddingOuter && chart.xPaddingOuter(0.5); + chart.yNice && chart.yNice(); const zoomChart = zoomableChart() .chart(chart) .settings(settings) - .xScale(xScale); + .xScale(xAxis.scale); const toolTip = nearbyTip() .settings(settings) - .xScale(xScale) - .yScale(yScale) + .xScale(xAxis.scale) + .yScale(yAxis.scale) .color(color) .data(data); diff --git a/packages/perspective-viewer-d3fc/src/js/data/groupData.js b/packages/perspective-viewer-d3fc/src/js/data/groupData.js index 555cc1f170..1774e847c6 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/groupData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/groupData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; import {splitIntoMultiSeries} from "./splitIntoMultiSeries"; export function groupData(settings, data) { diff --git a/packages/perspective-viewer-d3fc/src/js/data/heatmapData.js b/packages/perspective-viewer-d3fc/src/js/data/heatmapData.js index ac4a8be585..0cf4fb8113 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/heatmapData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/heatmapData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; export function heatmapData(settings, data) { const labelfn = labelFunction(settings); diff --git a/packages/perspective-viewer-d3fc/src/js/data/ohlcData.js b/packages/perspective-viewer-d3fc/src/js/data/ohlcData.js index 625dab45bf..9d264f14d0 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/ohlcData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/ohlcData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; import {splitIntoMultiSeries} from "./splitIntoMultiSeries"; export function ohlcData(settings, data) { diff --git a/packages/perspective-viewer-d3fc/src/js/data/pointData.js b/packages/perspective-viewer-d3fc/src/js/data/pointData.js index 21f05688b0..843ed1b479 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/pointData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/pointData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; import {splitIntoMultiSeries} from "./splitIntoMultiSeries"; export function pointData(settings, data) { diff --git a/packages/perspective-viewer-d3fc/src/js/data/splitAndBaseData.js b/packages/perspective-viewer-d3fc/src/js/data/splitAndBaseData.js index 2451584ad6..e60aa46faa 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/splitAndBaseData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/splitAndBaseData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; export function splitAndBaseData(settings, data) { const labelfn = labelFunction(settings); diff --git a/packages/perspective-viewer-d3fc/src/js/data/splitData.js b/packages/perspective-viewer-d3fc/src/js/data/splitData.js index 0aa6913890..1a44870c42 100644 --- a/packages/perspective-viewer-d3fc/src/js/data/splitData.js +++ b/packages/perspective-viewer-d3fc/src/js/data/splitData.js @@ -6,7 +6,7 @@ * the Apache License 2.0. The full license can be found in the LICENSE file. * */ -import {labelFunction} from "../axis/crossAxis"; +import {labelFunction} from "../axis/axisLabel"; export function splitData(settings, data) { const labelfn = labelFunction(settings); diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index 8aa44d53d7..ba0b713e66 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -47,9 +47,11 @@ function drawChart(chart) { const filtered = row_pivots.length > 0 ? json.filter(col => col.__ROW_PATH__ && col.__ROW_PATH__.length == row_pivots.length) : json; const dataMap = !row_pivots.length ? (col, i) => ({...removeHiddenData(col, hidden), __ROW_PATH__: [i]}) : col => removeHiddenData(col, hidden); + const aggregateType = agg => getOpType(row_pivots, agg.op, tschema[agg.column]); + let settings = { crossValues: row_pivots.map(r => ({name: r, type: tschema[r]})), - mainValues: aggregates.map(a => ({name: a.column, type: tschema[a.column]})), + mainValues: aggregates.map(a => ({name: a.column, type: aggregateType(a)})), splitValues: col_pivots.map(r => ({name: r, type: tschema[r]})), filter, data: filtered.map(dataMap) @@ -59,6 +61,20 @@ function drawChart(chart) { }; } +function getOpType(groupBy, op, varType) { + if (groupBy.length === 0) return varType; + + switch (op) { + case "count": + case "distinct count": + return "integer"; + case "mean": + case "mean by count": + return "float"; + } + return varType; +} + function removeHiddenData(col, hidden) { if (hidden && hidden.length > 0) { Object.keys(col).forEach(key => { diff --git a/packages/perspective-viewer-d3fc/src/js/series/barSeries.js b/packages/perspective-viewer-d3fc/src/js/series/barSeries.js index 365e32b8de..261b9c8e1a 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/barSeries.js +++ b/packages/perspective-viewer-d3fc/src/js/series/barSeries.js @@ -18,8 +18,26 @@ export function barSeries(settings, color) { }); return fc - .autoBandwidth(series) + .autoBandwidth(minBandwidth(series)) .crossValue(d => d.crossValue) .mainValue(d => d.mainValue) .baseValue(d => d.baseValue); } + +const minBandwidth = adaptee => { + const min = arg => { + return adaptee(arg); + }; + + fc.rebindAll(min, adaptee); + + min.bandwidth = (...args) => { + if (!args.length) { + return adaptee.bandwidth(); + } + adaptee.bandwidth(Math.max(args[0], 1)); + return min; + }; + + return min; +}; diff --git a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js index bf47cd2fc0..691e61951b 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js +++ b/packages/perspective-viewer-d3fc/src/js/series/seriesRange.js @@ -7,7 +7,7 @@ * */ import * as d3 from "d3"; -import {domain} from "../axis/mainAxis"; +import {domain} from "../axis/linearAxis"; export function seriesLinearRange(settings, data, valueName) { return d3.scaleLinear().domain( From f1b6fc56a6888ed1cde5049a28c09f5e656108e5 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Tue, 2 Apr 2019 10:02:42 +0100 Subject: [PATCH 35/47] Fixed themed tests --- packages/perspective-viewer-d3fc/test/html/themed-template.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/test/html/themed-template.html b/packages/perspective-viewer-d3fc/test/html/themed-template.html index 279a1318f9..0fcf6cfa1f 100644 --- a/packages/perspective-viewer-d3fc/test/html/themed-template.html +++ b/packages/perspective-viewer-d3fc/test/html/themed-template.html @@ -33,7 +33,7 @@ --column_type-string--color: #ff8888; --psp-text-field--border-color: #22a0ce; --button-hover--color: #22a0ce; - --d3fc-legend-text: #bbbbbb; + --d3fc-legend--text: #bbbbbb; --d3fc-axis--ticks: #bbbbbb; --d3fc-axis--lines: #555555; --d3fc-tooltip--background: #333333; From f443e758cad852c7a95ec615721e68056d44e0ad Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Tue, 2 Apr 2019 10:18:12 +0100 Subject: [PATCH 36/47] Added sunburst integration tests --- .../test/js/integration/sunburst.spec.js | 26 +++++++++++++++++++ .../test/results/results.json | 15 ++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 packages/perspective-viewer-d3fc/test/js/integration/sunburst.spec.js diff --git a/packages/perspective-viewer-d3fc/test/js/integration/sunburst.spec.js b/packages/perspective-viewer-d3fc/test/js/integration/sunburst.spec.js new file mode 100644 index 0000000000..99ede5ee55 --- /dev/null +++ b/packages/perspective-viewer-d3fc/test/js/integration/sunburst.spec.js @@ -0,0 +1,26 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ + +const path = require("path"); + +const utils = require("@jpmorganchase/perspective-viewer/test/js/utils.js"); +const simple_tests = require("@jpmorganchase/perspective-viewer/test/js/simple_tests.js"); + +const {withTemplate} = require("./simple-template"); +withTemplate("sunburst", "d3_sunburst"); + +utils.with_server({}, () => { + describe.page( + "sunburst.html", + () => { + simple_tests.default(); + }, + {reload_page: false, root: path.join(__dirname, "..", "..", "..")} + ); +}); diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index 59169b9d2b..5561c0a8c3 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -107,5 +107,18 @@ "candlestick.html/filter by a single instrument.": "36d43f5905d6e74cc622ee6d7be44894", "ohlc.html/filter by a single instrument.": "94d24188a2839ce1bd0b1dec71d20f33", "ohlc.html/filter to date range.": "3313f803361d232b1180e32b9a5d8c17", - "__GIT_COMMIT__": "2b976ce8bcbcb938207b4462a2e6d9e47df798cb" + "__GIT_COMMIT__": "f1b6fc56a6888ed1cde5049a28c09f5e656108e5", + "sunburst.html/shows a grid without any settings applied.": "322cc928bded452cf3e9156eda5b6bc1", + "sunburst.html/pivots by a row.": "92855908f1a166c5549d69a2ec61de9d", + "sunburst.html/pivots by two rows.": "d37493821e0656e7b669263505067111", + "sunburst.html/pivots by a column.": "e646effbf8160539424d969172eaf3ea", + "sunburst.html/pivots by a row and a column.": "7b459be8078c7663e9f79c3126137d4f", + "sunburst.html/pivots by two rows and two columns.": "6a3c60bca46a30b031fa1b80bebd2c13", + "sunburst.html/sorts by a hidden column.": "3d278235f76780a31dc55538171bb291", + "sunburst.html/sorts by a numeric column.": "13c691031d82971288944bcfc1e012ef", + "sunburst.html/filters by a numeric column.": "c58f9b74ba273715703391b536237675", + "sunburst.html/filters by a datetime column.": "2652910970a04728abaa73771e6fba69", + "sunburst.html/highlights invalid filter.": "afe668c4bc37b683c75cd2c74fc30b4a", + "sunburst.html/sorts by an alpha column.": "3c7c8151509c47d68d073cc6e228848a", + "sunburst.html/displays visible columns.": "be0b92259f73cc5ed768dde48ba061f1" } \ No newline at end of file From cd06f1ad7065e7dd40db9a62feea9f16aee63411 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 2 Apr 2019 11:41:25 +0100 Subject: [PATCH 37/47] Improved colour-legend display and labels (#143) * Use a "nice" number range for display purpose only * Improved colour-legend display Cleaner numbers (reduces display of fractions), and better display of "clean" range Give the colour legend a bit more height, but less width, which it doesn't need * Make legend components non-global Prevents legends from different charts interfering with each other Stores `pageIndex` in settings to preserve page position --- .../src/js/legend/colorRangeLegend.js | 16 +++-- .../src/js/legend/legend.js | 69 ++++++++++--------- .../src/js/legend/scrollableLegend.js | 23 ++++--- .../src/less/chart.less | 6 +- 4 files changed, 65 insertions(+), 49 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js b/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js index 7df6d4e249..223bd59daa 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js @@ -11,8 +11,8 @@ import * as fc from "d3fc"; import {getOrCreateElement} from "../utils/utils"; export function colorRangeLegend() { - const width = 100; - const height = 150; + const width = 90; + const height = 200; let scale = null; const xScale = d3 @@ -23,7 +23,10 @@ export function colorRangeLegend() { const formatFunc = d => (Number.isInteger(d) ? d3.format(",.0f")(d) : d3.format(",.2f")(d)); function legend(container) { - const domain = scale.domain(); + const domain = scale + .copy() + .nice() + .domain(); const paddedDomain = fc .extentLinear() .pad([0.1, 0.1]) @@ -47,16 +50,19 @@ export function colorRangeLegend() { selection.selectAll("path").style("fill", d => scale(d)); }); + const middle = domain[0] < 0 && domain[1] > 0 ? 0 : Math.round((domain[1] + domain[0]) / 2); + const tickValues = [...domain, middle]; + const axisLabel = fc .axisRight(yScale) - .tickValues([...domain, (domain[1] + domain[0]) / 2]) + .tickValues(tickValues) .tickSizeOuter(0) .tickFormat(d => formatFunc(d)); const legendSelection = getOrCreateElement(container, "div.legend-container", () => container .append("div") - .attr("class", "legend-container") + .attr("class", "legend-container legend-color") .style("z-index", "2") ); diff --git a/packages/perspective-viewer-d3fc/src/js/legend/legend.js b/packages/perspective-viewer-d3fc/src/js/legend/legend.js index d9351058c7..1a9d8fab65 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/legend.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/legend.js @@ -15,18 +15,22 @@ import {getChartElement} from "../plugin/root"; import {getOrCreateElement} from "../utils/utils"; import {draggableComponent} from "./styling/draggableComponent"; -const scrollColorLegend = scrollableLegend( - d3Legend - .legendColor() - .shape("circle") - .shapeRadius(6) -); -const scrollSymbolLegend = scrollableLegend( - d3Legend - .legendSymbol() - .shapePadding(1) - .labelOffset(3) -); +const scrollColorLegend = settings => + scrollableLegend( + d3Legend + .legendColor() + .shape("circle") + .shapeRadius(6), + settings + ); +const scrollSymbolLegend = settings => + scrollableLegend( + d3Legend + .legendSymbol() + .shapePadding(1) + .labelOffset(3), + settings + ); export const colorLegend = () => legendComponent(scrollColorLegend); export const symbolLegend = () => legendComponent(scrollSymbolLegend, symbolScale); @@ -42,7 +46,7 @@ function symbolScale(fromScale) { .range(range); } -function legendComponent(scrollLegend, scaleModifier) { +function legendComponent(scrollLegendComponent, scaleModifier) { let settings = {}; let scale = null; let color = null; @@ -50,6 +54,7 @@ function legendComponent(scrollLegend, scaleModifier) { function legend(container) { if (scale && scale.range().length > 1) { + const scrollLegend = scrollLegendComponent(settings); scrollLegend .scale(scale) .orient("vertical") @@ -71,6 +76,25 @@ function legendComponent(scrollLegend, scaleModifier) { const legendSelection = getOrCreateElement(container, "div.legend-container", () => container.append("div")); + scrollLegend.decorate(selection => { + const isHidden = data => settings.hideKeys && settings.hideKeys.includes(data); + + const cells = selection + .select("g.legendCells") + .attr("transform", "translate(20,20)") + .selectAll("g.cell"); + + cells.classed("hidden", isHidden); + cells.append("title").html(d => d); + + if (color) { + cells + .select("path") + .style("fill", d => (isHidden(d) ? null : color(d))) + .style("stroke", d => (isHidden(d) ? null : withoutOpacity(color(d)))); + } + }); + // render the legend legendSelection .attr("class", "legend-container") @@ -82,25 +106,6 @@ function legendComponent(scrollLegend, scaleModifier) { } } - scrollLegend.decorate(selection => { - const isHidden = data => settings.hideKeys && settings.hideKeys.includes(data); - - const cells = selection - .select("g.legendCells") - .attr("transform", "translate(20,20)") - .selectAll("g.cell"); - - cells.classed("hidden", isHidden); - cells.append("title").html(d => d); - - if (color) { - cells - .select("path") - .style("fill", d => (isHidden(d) ? null : color(d))) - .style("stroke", d => (isHidden(d) ? null : withoutOpacity(color(d)))); - } - }); - legend.settings = (...args) => { if (!args.length) { return settings; diff --git a/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js b/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js index 21cfbd71f4..18de9b4fe5 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/scrollableLegend.js @@ -8,24 +8,19 @@ */ import * as d3Legend from "d3-svg-legend"; import {rebindAll} from "d3fc"; -import {areArraysEqualSimple, getOrCreateElement} from "../utils/utils"; +import {getOrCreateElement} from "../utils/utils"; import legendControlsTemplate from "../../html/legend-controls.html"; import {cropCellContents} from "./styling/cropCellContents"; -export default fromLegend => { +export default (fromLegend, settings) => { const legend = fromLegend || d3Legend.legendColor(); - let domain = []; let pageCount = 1; let pageSize = 15; - let pageIndex = 0; + let pageIndex = settings.legend ? settings.legend.pageIndex : 0; let decorate = () => {}; const scrollableLegend = selection => { - const newDomain = legend.scale().domain(); - if (!areArraysEqualSimple(domain, newDomain)) { - pageIndex = 0; - domain = newDomain; - } + const domain = legend.scale().domain(); pageCount = Math.ceil(domain.length / pageSize); render(selection); @@ -48,7 +43,7 @@ export default fromLegend => { .attr("class", pageIndex === 0 ? "disabled" : "") .on("click", () => { if (pageIndex > 0) { - pageIndex--; + setPage(pageIndex - 1); render(selection); } }); @@ -58,7 +53,7 @@ export default fromLegend => { .attr("class", pageIndex >= pageCount - 1 ? "disabled" : "") .on("click", () => { if (pageIndex < pageCount - 1) { - pageIndex++; + setPage(pageIndex + 1); render(selection); } }); @@ -80,6 +75,11 @@ export default fromLegend => { decorate(selection); }; + const setPage = index => { + pageIndex = index; + settings.legend = {pageIndex}; + }; + const cellFilter = () => { return (_, i) => i >= pageSize * pageIndex && i < pageSize * pageIndex + pageSize; }; @@ -103,5 +103,6 @@ export default fromLegend => { }; rebindAll(scrollableLegend, legend); + return scrollableLegend; }; diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index c7c55afbd3..23e83e7a93 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -20,7 +20,7 @@ overflow: hidden; &.d3_heatmap { - padding-right: 165px; + padding-right: 105px; } &.d3_sunburst { @@ -151,6 +151,10 @@ left: auto; width: 150px; + &.legend-color { + width: 90px; + } + &[borderbox-on-hover="true"] { &:hover { box-shadow: 2px 2px 6px #000; From 83360b62d83138dd2c8e84e87a64ec15eab26971 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 2 Apr 2019 11:41:40 +0100 Subject: [PATCH 38/47] Tweaked sizing code for sunburst charts` (#144) * Tweaked sizing code for sunburst charts` Leads to large charts with more space usage, when divided into columns * Reduce scrolling if row height is "close enough" --- .../src/js/charts/sunburst.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 1c67517ec7..2f0fcf4502 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -20,8 +20,16 @@ function sunburst(container, settings) { const minSize = 500; const cols = sunburstData.length === 1 ? 1 : Math.floor(containerWidth / minSize); const rows = Math.ceil(sunburstData.length / cols); - container.style("grid-template-columns", `repeat(${cols}, ${containerWidth / cols}px)`); - container.style("grid-template-rows", `repeat(${rows}, ${containerHeight / cols}px)`); + const containerSize = { + width: containerWidth / cols, + height: Math.min(containerHeight, Math.max(containerHeight / rows, containerWidth / cols)) + }; + if (containerHeight / rows > containerSize.height * 0.75) { + containerSize.height = containerHeight / rows; + } + + container.style("grid-template-columns", `repeat(${cols}, ${containerSize.width}px)`); + container.style("grid-template-rows", `repeat(${rows}, ${containerSize.height}px)`); const sunburstDiv = container.selectAll("div.sunburst-container").data(treeData(settings), d => d.split); sunburstDiv.exit().remove(); @@ -48,7 +56,7 @@ function sunburst(container, settings) { .merge(sunburstDiv) .select("svg") .select("g.sunburst") - .attr("transform", `translate(${containerWidth / 2 / cols}, ${containerHeight / 2 / cols})`) + .attr("transform", `translate(${containerSize.width / 2}, ${containerSize.height / 2})`) .each(function({split, data}) { const sunburstElement = select(this); const svgNode = this.parentNode; @@ -57,7 +65,7 @@ function sunburst(container, settings) { const title = sunburstElement.select("text.title").text(split); title.attr("transform", `translate(0, ${-(height / 2 - 5)})`); - const radius = (Math.min(width, height) - 100) / 6; + const radius = (Math.min(width, height) - 120) / 6; const color = treeColor(settings, split, data.data.children); sunburstSeries() .settings(settings) From b5bee2623595434936eef99939a018c46b4aa3a4 Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 2 Apr 2019 12:22:10 +0100 Subject: [PATCH 39/47] Fixed sunburst legend overlap (#145) Allows for different chart types to have different size colour legends. Heatmap has space to have a taller one, but that it not so appropriate in sunburst --- .../src/js/legend/colorRangeLegend.js | 27 +++++++++---------- .../src/less/chart.less | 5 ++++ 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js b/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js index 223bd59daa..0aac6c2f79 100644 --- a/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js +++ b/packages/perspective-viewer-d3fc/src/js/legend/colorRangeLegend.js @@ -11,18 +11,24 @@ import * as fc from "d3fc"; import {getOrCreateElement} from "../utils/utils"; export function colorRangeLegend() { - const width = 90; - const height = 200; let scale = null; - const xScale = d3 - .scaleBand() - .domain([0, 1]) - .range([0, width]); - const formatFunc = d => (Number.isInteger(d) ? d3.format(",.0f")(d) : d3.format(",.2f")(d)); function legend(container) { + const legendSelection = getOrCreateElement(container, "div.legend-container", () => + container + .append("div") + .attr("class", "legend-container legend-color") + .style("z-index", "2") + ); + const {width, height} = legendSelection.node().getBoundingClientRect(); + + const xScale = d3 + .scaleBand() + .domain([0, 1]) + .range([0, width]); + const domain = scale .copy() .nice() @@ -59,13 +65,6 @@ export function colorRangeLegend() { .tickSizeOuter(0) .tickFormat(d => formatFunc(d)); - const legendSelection = getOrCreateElement(container, "div.legend-container", () => - container - .append("div") - .attr("class", "legend-container legend-color") - .style("z-index", "2") - ); - const legendSvg = getOrCreateElement(legendSelection, "svg", () => legendSelection.append("svg")) .style("width", width) .style("height", height); diff --git a/packages/perspective-viewer-d3fc/src/less/chart.less b/packages/perspective-viewer-d3fc/src/less/chart.less index 23e83e7a93..a4401f6443 100644 --- a/packages/perspective-viewer-d3fc/src/less/chart.less +++ b/packages/perspective-viewer-d3fc/src/less/chart.less @@ -153,6 +153,7 @@ &.legend-color { width: 90px; + height: 150px; } &[borderbox-on-hover="true"] { @@ -213,6 +214,10 @@ } } + .d3_heatmap .legend-container.legend-color { + height: 200px; + } + .zoom-controls { position: absolute; top: 10px; From 1d5f959336f62eb194019f55885dbbba31e89149 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Tue, 2 Apr 2019 13:39:23 +0100 Subject: [PATCH 40/47] Match arc opacity --- .../src/js/series/sunburst/sunburstClick.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js index 2202f10ac4..9834cd4d28 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js @@ -41,7 +41,7 @@ export const clickHandler = (data, g, parent, parentTitle, path, label, radius, .filter(function(d) { return +this.getAttribute("fill-opacity") || arcVisible(d.target); }) - .attr("fill-opacity", d => (arcVisible(d.target) ? 0.8 : 0)) + .attr("fill-opacity", d => (arcVisible(d.target) ? 1 : 0)) .attr("user-select", d => (arcVisible(d.target) ? "initial" : "none")) .attr("pointer-events", d => (arcVisible(d.target) ? "initial" : "none")) .attrTween("d", d => () => drawArc(radius)(d.current)); From e48fac38e9e696a75401442ab36495c5201d3400 Mon Sep 17 00:00:00 2001 From: Ro4052 Date: Tue, 2 Apr 2019 14:22:24 +0100 Subject: [PATCH 41/47] Sunburst centre now shows current level instead of parent --- .../src/js/series/sunburst/sunburstClick.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js index 9834cd4d28..1b9dba2aba 100644 --- a/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js +++ b/packages/perspective-viewer-d3fc/src/js/series/sunburst/sunburstClick.js @@ -16,7 +16,7 @@ export const clickHandler = (data, g, parent, parentTitle, path, label, radius, if (p.parent) { parent.datum(p.parent); parent.style("cursor", "pointer"); - parentTitle.html(`⇪ ${p.parent.data.name}`); + parentTitle.html(`⇪ ${p.data.name}`); } else { parent.datum(data); parent.style("cursor", "default"); From 5081a36d9adfba58b2285b54c6a9255e2fba0886 Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Tue, 2 Apr 2019 14:28:25 +0100 Subject: [PATCH 42/47] sunburst - add labels, don't display if no crossValue is supplied --- .../src/js/charts/sunburst.js | 2 ++ .../src/less/perspective-view.less | 14 +++++++++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 2f0fcf4502..3ee33bfd93 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -14,6 +14,8 @@ import {colorRangeLegend} from "../legend/colorRangeLegend"; import {tooltip} from "../tooltip/tooltip"; function sunburst(container, settings) { + if (settings.crossValues.length === 0) return; + const sunburstData = treeData(settings); const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); diff --git a/packages/perspective-viewer-d3fc/src/less/perspective-view.less b/packages/perspective-viewer-d3fc/src/less/perspective-view.less index 8d4c651e98..4a5556dca2 100644 --- a/packages/perspective-viewer-d3fc/src/less/perspective-view.less +++ b/packages/perspective-viewer-d3fc/src/less/perspective-view.less @@ -31,7 +31,7 @@ ); } -:host([view=d3_xy_scatter]), :host([view=d3_candlestick]), :host([view=d3_ohlc]) { +:host([view=d3_xy_scatter]), :host([view=d3_candlestick]), :host([view=d3_ohlc]), :host([view=d3_sunburst]) { & #app { &.columns_horizontal #side_panel #inactive_columns { padding-top:28px @@ -82,3 +82,15 @@ } } } + +:host([view=d3_sunburst]) { + & #app #side_panel #active_columns perspective-row { + &:nth-child(1):before { + content:"Size" + } + + &:nth-child(2):before { + content:"Color" + } + } +} From f41e561b6d809d4c77d771405799c8d48a681f4c Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Tue, 2 Apr 2019 14:54:24 +0100 Subject: [PATCH 43/47] update screenshots --- .../test/results/results.json | 54 +++++++++---------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index 5561c0a8c3..ba2bd505ae 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -1,5 +1,5 @@ { - "scatter.html/displays visible columns.": "0eb7b2524b257079b9b29fb934576bc9", + "scatter.html/displays visible columns.": "298c2b8768f55e0ce2aaf96b5c41aac9", "line.html/shows a grid without any settings applied.": "156d4d4b96e95b4764701861846caafe", "line.html/pivots by a row.": "878a4e0cbcd03e022efcf5ceb708f41f", "line.html/pivots by two rows.": "517b6e07f59b615e2f23774fa5a1aec8", @@ -90,35 +90,35 @@ "area.html/highlights invalid filter.": "12353e9b69e92b8ac31b3117d57484c1", "area.html/sorts by an alpha column.": "b5b4bb0defd5d76d0984b6862e5fcbd2", "area.html/displays visible columns.": "992fe96f6920748310abd45b752f2d32", - "heatmap.html/shows a grid without any settings applied.": "bce441e933348ab1bb8986392e4909b9", - "heatmap.html/pivots by a row.": "f72086b690a408213d5cc4844dc72533", - "heatmap.html/pivots by two rows.": "65a66c405a97d13888ef18d58dd9e828", - "heatmap.html/pivots by a column.": "24380b9a2088e1c5702f1a037ae7b520", - "heatmap.html/pivots by a row and a column.": "78ecf577b991670ea0b9520d3f3ec18c", - "heatmap.html/pivots by two rows and two columns.": "6138b0e17672528e80a0556a69c37cb2", - "heatmap.html/sorts by a hidden column.": "486189206f1e8db90772a6b3d05aab0f", - "heatmap.html/sorts by a numeric column.": "e2ddadc2dddc12fa336ed2837cd36f0c", - "heatmap.html/filters by a numeric column.": "75539031cc388a7f5ec6d468963c1b9a", - "heatmap.html/filters by a datetime column.": "7eb57bd0c2c731016c002435da4645fa", - "heatmap.html/highlights invalid filter.": "4b26893b6a811fda60c6befea2c661c1", - "heatmap.html/sorts by an alpha column.": "4390c31c4fd40385506aa8d56d8aebc8", - "heatmap.html/displays visible columns.": "4c89d56570fc7fd01a18485b8c2ffd0c", + "heatmap.html/shows a grid without any settings applied.": "1a9878730b6eed52151b8be8882b3b4c", + "heatmap.html/pivots by a row.": "9be8f535dc14f9a06cac8832bced2370", + "heatmap.html/pivots by two rows.": "8a2595f01cb45701cb1c5eff4543fd20", + "heatmap.html/pivots by a column.": "4aea164872d8dc6e4be7e1714f4d12aa", + "heatmap.html/pivots by a row and a column.": "0a8fe5a65f75bb8cb37c627849361cac", + "heatmap.html/pivots by two rows and two columns.": "928a98614ea8ee83c9c54609db066f3c", + "heatmap.html/sorts by a hidden column.": "a8b46d5acfed6284b3dfca5fda3407e6", + "heatmap.html/sorts by a numeric column.": "340ae0e0d202fb5bc7776c227d844c51", + "heatmap.html/filters by a numeric column.": "bb9aee3f85e73f2b10dcb31b7cc1bd4b", + "heatmap.html/filters by a datetime column.": "d225a2a306242bff8393b6343a7ea007", + "heatmap.html/highlights invalid filter.": "f7dd2e83f0e2795c888b7039895c4e84", + "heatmap.html/sorts by an alpha column.": "b675c0ff415fbc8b7ac96bbea84997f4", + "heatmap.html/displays visible columns.": "8d528026fc39c33ed169d555cad118aa", "candlestick.html/filter to date range.": "609fb900bb0ef5bbc8d98ab476abfd50", "candlestick.html/filter by a single instrument.": "36d43f5905d6e74cc622ee6d7be44894", "ohlc.html/filter by a single instrument.": "94d24188a2839ce1bd0b1dec71d20f33", "ohlc.html/filter to date range.": "3313f803361d232b1180e32b9a5d8c17", - "__GIT_COMMIT__": "f1b6fc56a6888ed1cde5049a28c09f5e656108e5", "sunburst.html/shows a grid without any settings applied.": "322cc928bded452cf3e9156eda5b6bc1", - "sunburst.html/pivots by a row.": "92855908f1a166c5549d69a2ec61de9d", - "sunburst.html/pivots by two rows.": "d37493821e0656e7b669263505067111", - "sunburst.html/pivots by a column.": "e646effbf8160539424d969172eaf3ea", - "sunburst.html/pivots by a row and a column.": "7b459be8078c7663e9f79c3126137d4f", - "sunburst.html/pivots by two rows and two columns.": "6a3c60bca46a30b031fa1b80bebd2c13", - "sunburst.html/sorts by a hidden column.": "3d278235f76780a31dc55538171bb291", - "sunburst.html/sorts by a numeric column.": "13c691031d82971288944bcfc1e012ef", - "sunburst.html/filters by a numeric column.": "c58f9b74ba273715703391b536237675", - "sunburst.html/filters by a datetime column.": "2652910970a04728abaa73771e6fba69", - "sunburst.html/highlights invalid filter.": "afe668c4bc37b683c75cd2c74fc30b4a", - "sunburst.html/sorts by an alpha column.": "3c7c8151509c47d68d073cc6e228848a", - "sunburst.html/displays visible columns.": "be0b92259f73cc5ed768dde48ba061f1" + "sunburst.html/pivots by a row.": "bda577fe9ee77dcf7c9a169e64d65f39", + "sunburst.html/pivots by two rows.": "883a07c0a09208f855ec1ab6927b581a", + "sunburst.html/pivots by a column.": "5c5ab3c7d0f607d99e50db09e1d49ff1", + "sunburst.html/pivots by a row and a column.": "3c762a6190e15004b517800a72f43d75", + "sunburst.html/pivots by two rows and two columns.": "4579e18669decf2cd4178f3804d01d10", + "sunburst.html/sorts by a hidden column.": "1f888056b9e4949f2b443b0737f8d7a1", + "sunburst.html/sorts by a numeric column.": "d4590b94b28e0771b0d5d0001cc43f28", + "sunburst.html/filters by a numeric column.": "cf71f6308d44440eb18e4dbcf4b5605b", + "sunburst.html/filters by a datetime column.": "82b3fbb7dbb0a21f0e4a763cb14cff7c", + "sunburst.html/highlights invalid filter.": "aedd70923b09d0e21a19ca719bc3d08d", + "sunburst.html/sorts by an alpha column.": "e1a00021ed856a8cf6693185b7f38a39", + "sunburst.html/displays visible columns.": "5be37b4d5f5c9d6b0d9563fa2ccfa856", + "__GIT_COMMIT__": "9152dfb80275bd2234fe5f876148476e243096a1" } \ No newline at end of file From 8955dc0b9ce686c0c1a5cc03142d214307ac191c Mon Sep 17 00:00:00 2001 From: Andy Lee Date: Tue, 2 Apr 2019 15:19:11 +0100 Subject: [PATCH 44/47] Allow for 2-column display when only 2 charts (#151) Previously, it would allocate 3 or more columns (space allowing), even if there were only two charts to show. --- packages/perspective-viewer-d3fc/src/js/charts/sunburst.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 3ee33bfd93..5ce2700f45 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -20,7 +20,7 @@ function sunburst(container, settings) { const {width: containerWidth, height: containerHeight} = container.node().getBoundingClientRect(); const minSize = 500; - const cols = sunburstData.length === 1 ? 1 : Math.floor(containerWidth / minSize); + const cols = Math.min(sunburstData.length, Math.floor(containerWidth / minSize)); const rows = Math.ceil(sunburstData.length / cols); const containerSize = { width: containerWidth / cols, From 2bf487b4924f78b1b28f4f7a2b71f99467d3585d Mon Sep 17 00:00:00 2001 From: Andy Date: Wed, 3 Apr 2019 14:11:22 +0100 Subject: [PATCH 45/47] Fix occasional integration test failure and some IE issues IE uses left/right instead of x/y and different format "transform". Use "closest" (with polyfill) to navigate up to the chart element. Polyfill "matches" which is used by d3fc Move default theme variables to where they actually need to be (IE/Edge don't get the variables otherwise) --- .../src/js/axis/ordinalAxis.js | 18 +++++----- .../src/js/plugin/plugin.js | 5 +++ .../src/js/plugin/polyfills/closest.js | 23 +++++++++++++ .../src/js/plugin/polyfills/index.js | 2 ++ .../src/js/plugin/polyfills/matches.js | 11 +++++++ .../src/less/perspective-view.less | 33 ------------------- .../perspective-viewer/src/less/viewer.less | 32 ++++++++++++++++++ 7 files changed, 83 insertions(+), 41 deletions(-) create mode 100644 packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js create mode 100644 packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js create mode 100644 packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js diff --git a/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js index c9e28c9160..16548ef9ba 100644 --- a/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js +++ b/packages/perspective-viewer-d3fc/src/js/axis/ordinalAxis.js @@ -151,11 +151,15 @@ export const component = settings => { }; const hideOverlappingLabels = (s, rotated) => { - const getTransformCoords = transform => - transform + const getTransformCoords = transform => { + const splitOn = transform.indexOf(",") !== -1 ? "," : " "; + const coords = transform .substring(transform.indexOf("(") + 1, transform.indexOf(")")) - .split(",") + .split(splitOn) .map(c => parseInt(c)); + while (coords.length < 2) coords.push(0); + return coords; + }; const rectanglesOverlap = (r1, r2) => r1.x <= r2.x + r2.width && r2.x <= r1.x + r1.width && r1.y <= r2.y + r2.height && r2.y <= r1.y + r1.height; const rotatedLabelsOverlap = (r1, r2) => r1.x + r1.width + 14 > r2.x + r2.width; @@ -189,17 +193,15 @@ export const component = settings => { }; const getXAxisBoundsRect = s => { - const chart = getChartContainer(s.node()) - .getRootNode() - .querySelector(".cartesian-chart"); + const chart = s.node().closest(".cartesian-chart"); const axis = chart.querySelector(".x-axis"); const chartRect = chart.getBoundingClientRect(); const axisRect = axis.getBoundingClientRect(); return { - x: chartRect.x - axisRect.x, + x: chartRect.left - axisRect.left, width: chartRect.width, - y: chartRect.y - axisRect.y, + y: chartRect.top - axisRect.top, height: chartRect.height }; }; diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js index ba0b713e66..06cd9eac1d 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/plugin.js @@ -8,6 +8,7 @@ */ import charts from "../charts/charts"; +import "./polyfills/index"; import "./template"; export const PRIVATE = Symbol("D3FC chart"); @@ -117,3 +118,7 @@ function deleteChart() { perspective_d3fc_element.delete(); } } + +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector; +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js new file mode 100644 index 0000000000..bf2da8264c --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/closest.js @@ -0,0 +1,23 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} + +if (!Element.prototype.closest) { + Element.prototype.closest = function(s) { + var el = this; + + do { + if (el.matches(s)) return el; + el = el.parentElement || el.parentNode; + } while (el !== null && el.nodeType === 1); + return null; + }; +} diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js new file mode 100644 index 0000000000..8cc9536b52 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/index.js @@ -0,0 +1,2 @@ +import "./matches"; +import "./closest"; diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js new file mode 100644 index 0000000000..96e76ea0b1 --- /dev/null +++ b/packages/perspective-viewer-d3fc/src/js/plugin/polyfills/matches.js @@ -0,0 +1,11 @@ +/****************************************************************************** + * + * Copyright (c) 2017, the Perspective Authors. + * + * This file is part of the Perspective library, distributed under the terms of + * the Apache License 2.0. The full license can be found in the LICENSE file. + * + */ +if (!Element.prototype.matches) { + Element.prototype.matches = Element.prototype.msMatchesSelector || Element.prototype.webkitMatchesSelector; +} diff --git a/packages/perspective-viewer-d3fc/src/less/perspective-view.less b/packages/perspective-viewer-d3fc/src/less/perspective-view.less index 4a5556dca2..fc869c15a8 100644 --- a/packages/perspective-viewer-d3fc/src/less/perspective-view.less +++ b/packages/perspective-viewer-d3fc/src/less/perspective-view.less @@ -1,36 +1,3 @@ -:host { - --d3fc-series: rgba(31, 119, 180, 0.5); - --d3fc-series-1: #0366d6; - --d3fc-series-2: #ff7f0e; - --d3fc-series-3: #2ca02c; - --d3fc-series-4: #d62728; - --d3fc-series-5: #9467bd; - --d3fc-series-6: #8c564b; - --d3fc-series-7: #e377c2; - --d3fc-series-8: #7f7f7f; - --d3fc-series-9: #bcbd22; - --d3fc-series-10: #17becf; - --d3fc-gradient-full: linear-gradient( - #4d342f 0%, - #e4521b 22.5%, - #feeb65 42.5%, - #f0f0f0 50%, - #dcedc8 57.5%, - #42b3d5 67.5%, - #1a237e 100% - ); - --d3fc-gradient-positive: linear-gradient( - #dcedc8 0%, - #42b3d5 35%, - #1a237e 100% - ); - --d3fc-gradient-negative: linear-gradient( - #feeb65 100%, - #e4521b 70%, - #4d342f 0% - ); -} - :host([view=d3_xy_scatter]), :host([view=d3_candlestick]), :host([view=d3_ohlc]), :host([view=d3_sunburst]) { & #app { &.columns_horizontal #side_panel #inactive_columns { diff --git a/packages/perspective-viewer/src/less/viewer.less b/packages/perspective-viewer/src/less/viewer.less index 5c761146f8..ca6703f259 100644 --- a/packages/perspective-viewer/src/less/viewer.less +++ b/packages/perspective-viewer/src/less/viewer.less @@ -75,6 +75,38 @@ --hypergrid-row-hover--color: #666; --hypergrid-cell-hover--background: #eeeeee; --hypergrid-cell-hover--color: #666; + + --d3fc-series: rgba(31, 119, 180, 0.5); + --d3fc-series-1: #0366d6; + --d3fc-series-2: #ff7f0e; + --d3fc-series-3: #2ca02c; + --d3fc-series-4: #d62728; + --d3fc-series-5: #9467bd; + --d3fc-series-6: #8c564b; + --d3fc-series-7: #e377c2; + --d3fc-series-8: #7f7f7f; + --d3fc-series-9: #bcbd22; + --d3fc-series-10: #17becf; + --d3fc-gradient-full: linear-gradient( + #4d342f 0%, + #e4521b 22.5%, + #feeb65 42.5%, + #f0f0f0 50%, + #dcedc8 57.5%, + #42b3d5 67.5%, + #1a237e 100% + ); + --d3fc-gradient-positive: linear-gradient( + #dcedc8 0%, + #42b3d5 35%, + #1a237e 100% + ); + --d3fc-gradient-negative: linear-gradient( + #feeb65 100%, + #e4521b 70%, + #4d342f 0% + ); + #pivot_chart { position: absolute; width: 100%; From e4c9e71b17cbfefe2f86f9296f6009b625406a59 Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Wed, 3 Apr 2019 15:54:28 +0100 Subject: [PATCH 46/47] add parameter names for sunburst --- packages/perspective-viewer-d3fc/src/js/charts/sunburst.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js index 5ce2700f45..70c466401f 100644 --- a/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js +++ b/packages/perspective-viewer-d3fc/src/js/charts/sunburst.js @@ -92,7 +92,8 @@ sunburst.plugin = { max_size: 25000, initial: { type: "number", - count: 2 + count: 2, + names: ["Size", "Color"] } }; From 2773b1866268201738d992e646c1e32462f6663a Mon Sep 17 00:00:00 2001 From: matt-hooper Date: Wed, 3 Apr 2019 16:11:26 +0100 Subject: [PATCH 47/47] remove unnecessary initialiseStyles and refresh screenshots --- packages/perspective-viewer-d3fc/src/js/plugin/template.js | 2 -- packages/perspective-viewer-d3fc/test/results/results.json | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/perspective-viewer-d3fc/src/js/plugin/template.js b/packages/perspective-viewer-d3fc/src/js/plugin/template.js index 8bb5d0d616..2451c5a3a7 100644 --- a/packages/perspective-viewer-d3fc/src/js/plugin/template.js +++ b/packages/perspective-viewer-d3fc/src/js/plugin/template.js @@ -23,8 +23,6 @@ class D3FCChartElement extends HTMLElement { this._container = this.shadowRoot.querySelector(".chart"); this._chart = null; this._settings = null; - - initialiseStyles(this._container); } render(chart, settings) { diff --git a/packages/perspective-viewer-d3fc/test/results/results.json b/packages/perspective-viewer-d3fc/test/results/results.json index ba2bd505ae..d9eb926ec2 100644 --- a/packages/perspective-viewer-d3fc/test/results/results.json +++ b/packages/perspective-viewer-d3fc/test/results/results.json @@ -119,6 +119,6 @@ "sunburst.html/filters by a datetime column.": "82b3fbb7dbb0a21f0e4a763cb14cff7c", "sunburst.html/highlights invalid filter.": "aedd70923b09d0e21a19ca719bc3d08d", "sunburst.html/sorts by an alpha column.": "e1a00021ed856a8cf6693185b7f38a39", - "sunburst.html/displays visible columns.": "5be37b4d5f5c9d6b0d9563fa2ccfa856", - "__GIT_COMMIT__": "9152dfb80275bd2234fe5f876148476e243096a1" + "sunburst.html/displays visible columns.": "65c73653a7c8b55ea823d2b90cc72b59", + "__GIT_COMMIT__": "e4c9e71b17cbfefe2f86f9296f6009b625406a59" } \ No newline at end of file