From c00affc6e67f99c61a85ed1f4c6d2d98bfe42413 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:03:34 -0400 Subject: [PATCH 01/68] Create custom heatmap model --- packages/core/src/model/heatmap.ts | 434 +++++++++++++++++++++++++++++ 1 file changed, 434 insertions(+) create mode 100644 packages/core/src/model/heatmap.ts diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts new file mode 100644 index 0000000000..5655c25afd --- /dev/null +++ b/packages/core/src/model/heatmap.ts @@ -0,0 +1,434 @@ +// Internal Imports +import { ScaleTypes } from '../interfaces'; +import { ChartModelCartesian } from './cartesian-charts'; +import { Tools } from '../tools'; + +// date formatting +import { format } from 'date-fns'; + +// d3 imports +import { extent } from 'd3-array'; +import { scaleLinear } from 'd3-scale'; + +/** The gauge chart model layer */ +export class HeatmapModel extends ChartModelCartesian { + private palettes = { + // Monochromatic palettes + // Purple 10 - 100 (Includes white) + purple: [ + '#ffffff', + '#f6f2ff', + '#e8daff', + '#d4bbff', + '#be95ff', + '#a56eff', + '#8a3ffc', + '#6929c4', + '#491d8b', + '#31135e', + '#1c0f30', + ], + // Blue 10 - 100 (Includes white) + blue: [ + '#ffffff', + '#edf5ff', + '#d0e2ff', + '#a6c8ff', + '#78a9ff', + '#4589ff', + '#0f62fe', + '#0043ce', + '#002d9c', + '#001d6c', + '#001141', + ], + // Cyan 10 - 100 (Includes white) + cyan: [ + '#ffffff', + '#e5f6ff', + '#bae6ff', + '#82cfff', + '#33b1ff', + '#1192e8', + '#0072c3', + '#00539a', + '#003a6d', + '#012749', + '#1c0f30', + ], + // Teal 10 - 100 (includes white) + teal: [ + '#ffffff', + '#d9fbfb', + '#9ef0f0', + '#3ddbd9', + '#08bdba', + '#009d9a', + '#007d79', + '#005d5d', + '#004144', + '#022b30', + '#081a1c', + ], + // Diverging palettes + // Red 80 - 10, Cyan 10 - 80 + 'red-cyan': [ + '#750e13', + '#a2191f', + '#da1e28', + '#fa4d56', + '#ff8389', + '#ffb3b8', + '#ffd7d9', + '#fff1f1', + '#e5f6ff', + '#bae6ff', + '#82cfff', + '#33b1ff', + '#1192e8', + '#0072c3', + '#00539a', + '#003a6d', + ], + // Purple 80 - 10, Teal 10 - 80 + 'purple-teal': [ + '#491d8b', + '#6929c4', + '#8a3ffc', + '#a56eff', + '#be95ff', + '#d4bbff', + '#e8daff', + '#f6f2ff', + '#d9fbfb', + '#9ef0f0', + '#3ddbd9', + '#08bdba', + '#009d9a', + '#007d79', + '#005d5d', + '#004144', + ], + }; + + // Will hold linearScale used in tick creation & colors + private _linearScale: any = undefined; + private _colorScale: any = undefined; + + // List of unique ranges and domains + private _domains = []; + private _range = []; + + private _matrix = {}; + + constructor(services: any) { + super(services); + } + + getLinearScale() { + if (!this._linearScale) { + this._linearScale = scaleLinear() + .domain(this.getValueDomain()) + .range([0, 75]); + } + + return this._linearScale; + } + + /** + * Get min and maximum value of the display data + * @returns Array consisting of smallest and largest values in data + */ + getValueDomain() { + const data = this.getDisplayData().map((element) => element.value); + const limits = extent(data); + const domain = []; + + // Round extent values to the nearest 10th values since axis rounds values to multiples of 2, 5, and 10s. + limits.forEach((number) => { + let value = Number(number); + + if (value % 10 === 0 || value === 0) { + value; + } else if (value < 0) { + value = Math.floor(value / 10) * 10; + } else { + value = Math.ceil(value / 10) * 10; + } + + domain.push(value); + }); + + return domain; + } + + /** + * + * @param value + * @returns + */ + getFillColor(value: number) { + if (!this._colorScale) { + this.getColorScale(); + } + + return this._colorScale(value); + } + + /** + * Returns linear color scale + * @returns Scale + */ + getColorScale() { + if (!this._colorScale) { + this._colorScale = scaleLinear() + /** + * @todo + * If getpalette() returns array of size 2 + * Check to see if, there is negative numbers and positive numbers + * If Yes, then use min and max values + * + * OR + * + * IN getTicks, compare length of `colors` and `ticks(return)` variable. + * If they do not match, use min and max, and the first and last color. + */ + .domain(this.getTicks()) + .range(this.getPalettes()); + } + + return this._colorScale; + } + + /** + * Generate a list of all unique domains + * @returns String[] + */ + getUniqueDomain(): string[] { + if (Tools.isEmpty(this._domains)) { + const displayData = this.getDisplayData(); + const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); + + // Get unique axis values & create a matrix + this._domains = Array.from( + new Set( + displayData.map((d) => { + return d[domainIdentifier]; + }) + ) + ); + } + + return this._domains; + } + + /** + * Generates a list of all unique ranges + * @returns String[] + */ + getUniqueRanges(): string[] { + if (Tools.isEmpty(this._range)) { + const displayData = this.getDisplayData(); + const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); + + // Get unique axis values & create a matrix + this._range = Array.from( + new Set( + displayData.map((d) => { + return d[rangeIdentifier]; + }) + ) + ); + } + + return this._range; + } + + /** + * Generates a matrix (If doesn't exist) and returns it + * @returns Object + */ + getMatrix() { + if (Tools.isEmpty(this._matrix)) { + const uniqueDomain = this.getUniqueDomain(); + const uniqueRange = this.getUniqueRanges(); + + const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); + const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); + + // Create matrix (domain by range) and initalize it's values to null + uniqueDomain.forEach((dom: any) => { + const range = {}; + // Data will be set to null by default, to signify 'missing' + uniqueRange.forEach((element: any) => { + range[element] = { + value: null, + index: -1, + }; + }); + this._matrix[dom] = range; + }); + + // Fill in user passed data + this.getDisplayData().forEach((d, i) => { + this._matrix[d[domainIdentifier]][d[rangeIdentifier]] = { + value: d['value'], + index: i, + }; + }); + } + + return this._matrix; + } + + /** + * Converts Object matrix into a single array + * @returns Object[] + */ + getMatrixAsArray(): Object[] { + if (Tools.isEmpty(this._matrix)) { + this.getMatrix(); + } + + const uniqueDomain = this.getUniqueDomain(); + const uniqueRange = this.getUniqueRanges(); + /** + * @todo + * - Multiply uniqueDomain.length by uniqueRange.length to get total possible values + * - If displayData().length matches array multiple, return displayData to improve performance + */ + + const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); + const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); + + const arr = []; + uniqueDomain.forEach((domain) => { + uniqueRange.forEach((range) => { + const element = { + value: this._matrix[domain][range].value, + index: this._matrix[domain][range].index, + }; + element[domainIdentifier] = domain; + element[rangeIdentifier] = range; + arr.push(element); + }); + }); + + return arr; + } + + /** + * Generate ticks to display based on available colors in list + * @returns Array + */ + getTicks() { + const extent = this.getValueDomain(); + const colors = this.getPalettes().length; + return scaleLinear() + .domain([extent[0], extent[1]]) + .nice() + .ticks(colors); + } + + /** + * Generate tabular data from display data + * @returns Array + */ + getTabularDataArray() { + const displayData = this.getDisplayData(); + + const { cartesianScales } = this.services; + const { + primaryDomain, + primaryRange, + secondaryDomain, + secondaryRange, + } = this.assignRangeAndDomains(); + + const domainScaleType = cartesianScales.getDomainAxisScaleType(); + let domainValueFormatter; + if (domainScaleType === ScaleTypes.TIME) { + domainValueFormatter = (d) => format(d, 'MMM d, yyyy'); + } + + const result = [ + [ + primaryDomain.label, + primaryRange.label, + ...(secondaryDomain ? [secondaryDomain.label] : []), + ...(secondaryRange ? [secondaryRange.label] : []), + 'Value', + ], + ...displayData.map((datum) => [ + datum[primaryDomain.identifier] === null + ? '–' + : domainValueFormatter + ? domainValueFormatter(datum[primaryDomain.identifier]) + : datum[primaryDomain.identifier], + datum[primaryRange.identifier] === null || + isNaN(datum[primaryRange.identifier]) + ? '–' + : datum[primaryRange.identifier].toLocaleString(), + ...(secondaryDomain + ? [ + datum[secondaryDomain.identifier] === null + ? '–' + : datum[secondaryDomain.identifier], + ] + : []), + ...(secondaryRange + ? [ + datum[secondaryRange.identifier] === null || + isNaN(datum[secondaryRange.identifier]) + ? '–' + : datum[secondaryRange.identifier], + ] + : []), + datum['value'], + ]), + ]; + + return result; + } + + /** + * Returns colors + * @returns Array + */ + getPalettes() { + const type = Tools.getProperty( + this.getOptions(), + 'heatmap', + 'colorPalette', + 'type' + ); + + const customColorPalette = Tools.getProperty( + this.getOptions(), + 'heatmap', + 'colorPalette', + 'colorCodes' + ); + + // If user pass in custom colors, use custom colors + if (customColorPalette?.length) { + return customColorPalette; + } + + // If domain consists of negative and positive values, use diverging palettes + const domain = this.getValueDomain(); + if (domain[0] < 0 && domain[1] > 0) { + // If type is not set to available options, use default + if (type !== 'red-cyan' && type !== 'purple-teal') { + return this.palettes['red-cyan']; + } + } + + // Check if value exists, if it doesn't use default + if (!type || !this.palettes[type]) { + return this.palettes['purple']; + } + + return this.palettes[type]; + } +} From 7ce44e46ffb93e01222f79361245f8d28030c783 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:06:11 -0400 Subject: [PATCH 02/68] Create heatmap interface --- packages/core/src/interfaces/charts.ts | 40 ++++++++++++++++++++++++++ packages/core/src/interfaces/enums.ts | 9 ++++++ packages/core/src/interfaces/events.ts | 12 +++++++- 3 files changed, 60 insertions(+), 1 deletion(-) diff --git a/packages/core/src/interfaces/charts.ts b/packages/core/src/interfaces/charts.ts index 04fb130381..429e3fdd34 100644 --- a/packages/core/src/interfaces/charts.ts +++ b/packages/core/src/interfaces/charts.ts @@ -5,6 +5,7 @@ import { Alignments, ChartTypes, TreeTypes, + DividerStatus, } from './enums'; import { LegendOptions, @@ -510,3 +511,42 @@ export interface AlluvialChartOptions extends BaseChartOptions { monochrome?: boolean; }; } + +/** + * options specific to Heatmap charts + */ +export interface HeatmapChartOptions extends BaseChartOptions { + heatmap: { + /** + * Divider width state - will default to auto + * No cell divider for cell dimensions less than 16 + */ + divider?: { + state?: DividerStatus; + }; + /** + * @question - Should this be a new config? I'm not sure if color legend will be reused + * Color palette too use + */ + colorPalette?: { + /** + * @question - REQUIRES IMPLEMENTATION REVIEW + * - SHOULD THIS BE PART OF THE COLOR OBJECT INSTEAD? + * Sets which IBM color scheme to use, defaults to 'purple' + */ + type?: + | 'purple' + | 'blue' + | 'cyan' + | 'teal' + | 'red-cyan' + | 'purple-teal'; + /** + * @question - REQUIRES IMPLEMENTATION REVIEW + * - SHOULD THIS BE PART OF THE LEGEND OR COLOR OBJECT INSTEAD? + * Uses the listed colors to generate color scheme (For both heatmap & color-legend); + */ + colorCodes?: Array; + }; + }; +} diff --git a/packages/core/src/interfaces/enums.ts b/packages/core/src/interfaces/enums.ts index 3bf3ade715..e42a85b324 100644 --- a/packages/core/src/interfaces/enums.ts +++ b/packages/core/src/interfaces/enums.ts @@ -250,3 +250,12 @@ export enum LegendItemType { QUARTILE = 'quartile', ZOOM = 'zoom', } + +/** + * enum of axis ticks rotation + */ +export enum DividerStatus { + ON = 'on', + AUTO = 'auto', + OFF = 'off', +} diff --git a/packages/core/src/interfaces/events.ts b/packages/core/src/interfaces/events.ts index 6ae9559da2..257001a208 100644 --- a/packages/core/src/interfaces/events.ts +++ b/packages/core/src/interfaces/events.ts @@ -167,7 +167,7 @@ export enum Radar { export enum Tree { NODE_MOUSEOVER = 'tree-node-mouseover', NODE_CLICK = 'tree-node-click', - NODE_MOUSEOUT = 'tree-node-mouseout' + NODE_MOUSEOUT = 'tree-node-mouseout', } /** @@ -240,3 +240,13 @@ export enum Meter { METER_MOUSEOUT = 'meter-mouseout', METER_MOUSEMOVE = 'meter-mousemove', } + +/** + * enum of all heatmap related events + */ +export enum Heatmap { + HEATMAP_MOUSEOVER = 'heatmap-mouseover', + HEATMAP_CLICK = 'heatmap-click', + HEATMAP_MOUSEOUT = 'heatmap-mouseout', + HEATMAP_MOUSEMOVE = 'hetmap-mousemove', +} From e473704b24b4ccd083564bb1fcd208660ad4ea5d Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:06:45 -0400 Subject: [PATCH 03/68] Create new color legend --- .../essentials/color-scale-legend.ts | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 packages/core/src/components/essentials/color-scale-legend.ts diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts new file mode 100644 index 0000000000..034e1f8fae --- /dev/null +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -0,0 +1,89 @@ +// Internal Imports +import { Tools } from '../../tools'; +import { + Alignments, + RenderTypes, + Roles, + Events, + TruncationTypes, +} from '../../interfaces'; +import * as Configuration from '../../configuration'; + +import { Legend } from './legend'; +import { DOMUtils } from '../../services'; + +// D3 imports +import { axisBottom } from 'd3-axis'; + +export class ColorScaleLegend extends Legend { + type = 'color-legend'; + renderType = RenderTypes.SVG; + + private gradient_id = + 'gradient-id-' + Math.floor(Math.random() * 99999999999); + + render() { + // svg and container widths + const svg = this.getComponentContainer({ withinChartClip: true }); + svg.html(''); + const { width, height } = DOMUtils.getSVGElementSize(svg, { + useAttrs: true, + }); + + const options = this.getOptions(); + const legendOptions = Tools.getProperty(options, 'legend'); + + const colors = this.model.getPalettes(); + const ticks = this.model.getTicks(); + const linarScale = this.model.getLinearScale().range([0, 250]); + const stopLength = 100 / colors.length; + + /** + * @todo - Add legend orientation support + * Need designer feedback? + */ + const legendOrientation = Tools.getProperty( + options, + 'legend', + 'orientation' + ); + + const group = svg + .append('g') + /** + * @todo - Determine translation value so that initial value isn't trimmed + */ + .attr('transform', `translate(18, 0)`); + + // Generate the gradient + const linearGradient = group + .append('linearGradient') + .attr('id', (d) => `${this.gradient_id}-legend`) + .selectAll('stop') + .data(colors) + .enter() + .append('stop') + .attr('offset', (d, i) => `${i * stopLength}%`) + .attr('stop-color', (d) => d); + + const rectangle = group + .append('rect') + /** + * @todo - determine width & height + * x offset (or padding) to prevent the first letter from being clipped + */ + .attr('width', '250px') + .attr('height', '18px') + .style('fill', `url(#${this.gradient_id}-legend)`); + + const xAxis = axisBottom(linarScale).tickSize(0).tickValues(ticks); + + // Align axes at the bottom of the rectangle and delete the domain line + group + .append('g') + .attr('transform', 'translate(0,18)') + .call(xAxis) + .select('.domain') + .remove(); + } +} From 42b0ef49b421732ae069977d54752d0685771eda Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:07:17 -0400 Subject: [PATCH 04/68] Create heatmap component --- packages/core/src/charts/heatmap.ts | 52 +++ .../core/src/components/graphs/heatmap.ts | 407 ++++++++++++++++++ 2 files changed, 459 insertions(+) create mode 100644 packages/core/src/charts/heatmap.ts create mode 100644 packages/core/src/components/graphs/heatmap.ts diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts new file mode 100644 index 0000000000..eb8475046a --- /dev/null +++ b/packages/core/src/charts/heatmap.ts @@ -0,0 +1,52 @@ +// Internal Imports +import { HeatmapModel } from '../model/heatmap'; +import { AxisChart } from '../axis-chart'; +import * as Configuration from '../configuration'; +import { ChartConfig, HeatmapChartOptions } from '../interfaces/index'; +import { Tools } from '../tools'; + +// Components +import { + Heatmap, + TwoDimensionalAxes, + // the imports below are needed because of typescript bug (error TS4029) + Tooltip, + ColorScaleLegend, + LayoutComponent, +} from '../components/index'; + +export class HeatmapChart extends AxisChart { + model = new HeatmapModel(this.services); + + constructor( + holder: Element, + chartConfigs: ChartConfig + ) { + super(holder, chartConfigs); + + // Merge the default options for this chart + // With the user provided options + this.model.setOptions( + Tools.mergeDefaultChartOptions( + Configuration.options.heatmapChart, + chartConfigs.options + ) + ); + + // Initialize data, services, components etc. + this.init(holder, chartConfigs); + } + + getComponents() { + // Specify what to render inside the graph-frame + const graphFrameComponents = [ + new TwoDimensionalAxes(this.model, this.services), + new Heatmap(this.model, this.services), + ]; + + const components: any[] = this.getAxisChartComponents( + graphFrameComponents + ); + return components; + } +} diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts new file mode 100644 index 0000000000..f63270cd66 --- /dev/null +++ b/packages/core/src/components/graphs/heatmap.ts @@ -0,0 +1,407 @@ +// Internal Imports +import { Component } from '../component'; +import { DOMUtils } from '../../services'; +import * as Configuration from '../../configuration'; +import { Events, RenderTypes, DividerStatus } from '../../interfaces'; +import { Tools } from '../../tools'; + +import { get } from 'lodash-es'; + +// D3 Imports +import { select } from 'd3-selection'; + +export class Heatmap extends Component { + type = 'heatmap'; + renderType = RenderTypes.SVG; + + private matrix = {}; + private xBandwidth = 0; + private yBandwidth = 0; + + init() { + const eventsFragment = this.services.events; + + // Highlight correct circle on legend item hovers + eventsFragment.addEventListener( + Events.Axis.LABEL_MOUSEOVER, + this.handleAxisOnHover + ); + + // Un-highlight circles on legend item mouseouts + eventsFragment.addEventListener( + Events.Axis.LABEL_MOUSEOUT, + this.handleAxisMouseOut + ); + } + + render(animate = true) { + // svg and container widths + const svg = this.getComponentContainer({ withinChartClip: true }); + + const { cartesianScales } = this.services; + const options = this.model.getOptions(); + this.matrix = this.model.getMatrix(); + + svg.html(''); + + // determine x and y axis scale + const mainXScale = cartesianScales.getMainXScale(); + const mainYScale = cartesianScales.getMainYScale(); + const domainIdentifier = cartesianScales.getDomainIdentifier(); + const rangeIdentifier = cartesianScales.getRangeIdentifier(); + + // Get unique axis values & create a matrix + const uniqueDomain = this.model.getUniqueDomain(); + const uniqueRange = this.model.getUniqueRanges(); + + // Get matrix in the form of an array to create a single heatmap group + const matrixArray = this.model.getMatrixAsArray(); + + // Get available chart area + const xRange = mainXScale.range(); + const yRange = mainYScale.range(); + + // Determine rectangle dimensions based on the number of unique domain and range + this.xBandwidth = Math.abs( + (xRange[1] - xRange[0]) / uniqueDomain.length + ); + this.yBandwidth = Math.abs( + (yRange[1] - yRange[0]) / uniqueRange.length + ); + + const rectangles = svg + .selectAll() + .data(matrixArray) + .enter() + .append('rect') + .attr('class', (d) => `heat-${d.index}`) + .classed('heat', true) + .classed('null-state', (d) => + d.index === -1 || d.value === null ? true : false + ) + .attr('x', (d) => mainXScale(d[domainIdentifier])) + .attr('y', (d) => mainYScale(d[rangeIdentifier])) + .attr('width', this.xBandwidth) + .attr('height', this.yBandwidth) + .style('fill', (d) => { + // Check if a valid value exists + if (d.index === -1 || d.value === null) { + /** + * @question - Determine what colors should be displaced based on theme for empty cells + * Class null-state, can be used to assign fill color in scss + */ + return '#f4f4f4'; + } + return this.model.getFillColor(Number(d.value)); + }) + .attr('aria-label', (d) => d.value); + + // Add dividers if status is not off, will assume auto or on by default. + const dividerStatus = Tools.getProperty( + options, + 'heatmap', + 'divider', + 'state' + ); + + // Add cell divider based on status + if (dividerStatus !== DividerStatus.OFF) { + if ( + (dividerStatus === DividerStatus.AUTO && + Configuration.heatmap.minCellDividerDimension <= + this.xBandwidth && + Configuration.heatmap.minCellDividerDimension <= + this.yBandwidth) || + dividerStatus === DividerStatus.ON + ) { + /** + * @question + * Use Gray 10 on white theme, but what about the others? + */ + rectangles + .style('stroke', '#f4f4f4') + .style('stroke-width', '1px'); + } + } + + this.addEventListener(); + } + + addEventListener() { + const self = this; + const { cartesianScales } = this.services; + const options = this.getOptions(); + const totalLabel = get(options, 'tooltip.totalLabel'); + // Add dividers if status is not off, will presume auto or on by default. + const dividerStatus = Tools.getProperty( + options, + 'heatmap', + 'divider', + 'state' + ); + + const domainIdentifier = cartesianScales.getDomainIdentifier(); + const rangeIdentifier = cartesianScales.getRangeIdentifier(); + + const domainLabel = cartesianScales.getDomainLabel(); + const rangeLabel = cartesianScales.getRangeLabel(); + + this.parent + .selectAll('rect.heat') + .on('mouseover', function (event, datum) { + const hoveredElement = select(this); + const nullState = hoveredElement.classed('null-state'); + + // Dispatch event and tooltip only if value exists + if (!nullState) { + const fillColor = hoveredElement.style('fill'); + + // Dispatch mouse over event + self.services.events.dispatchEvent( + Events.Heatmap.HEATMAP_MOUSEOVER, + { + event, + element: hoveredElement, + datum: datum, + } + ); + + // Perform visual changes only if value is valid + // Highlight element + hoveredElement + .raise() + .classed('raised', true) + .style('stroke', 'white') + .style('stroke-width', '3px'); + + // Dispatch tooltip show event + self.services.events.dispatchEvent(Events.Tooltip.SHOW, { + event, + items: [ + { + label: domainLabel, + value: datum[domainIdentifier], + }, + { + label: rangeLabel, + value: datum[rangeIdentifier], + }, + { + label: totalLabel || 'Total', + value: datum['value'], + color: fillColor, + }, + ], + }); + } + }) + .on('mousemove', function (event, datum) { + // Dispatch mouse move event + self.services.events.dispatchEvent( + Events.Heatmap.HEATMAP_MOUSEMOVE, + { + event, + element: select(this), + datum: datum, + } + ); + // Dispatch tooltip move event + self.services.events.dispatchEvent(Events.Tooltip.MOVE, { + event, + }); + }) + .on('click', function (event, datum) { + // Dispatch mouse click event + self.services.events.dispatchEvent( + Events.Heatmap.HEATMAP_CLICK, + { + event, + element: select(this), + datum: datum, + } + ); + }) + .on('mouseout', function (event, datum) { + const hoveredElement = select(this); + const nullState = hoveredElement.classed('null-state'); + hoveredElement.classed('raised', false); + + // Add cell divider based on status + if (dividerStatus !== DividerStatus.OFF && !nullState) { + if ( + (dividerStatus === DividerStatus.AUTO && + Configuration.heatmap.minCellDividerDimension <= + self.xBandwidth && + Configuration.heatmap.minCellDividerDimension <= + self.yBandwidth) || + dividerStatus === DividerStatus.ON + ) { + /** + * @question + * Use Gray 10 on white theme, but what about the others? + */ + hoveredElement + .style('stroke', '#f4f4f4') + .style('stroke-width', '1px'); + } else { + hoveredElement + .style('stroke', 'none') + .style('stroke-width', '0px'); + } + } + + // Dispatch mouse out event + self.services.events.dispatchEvent( + Events.Heatmap.HEATMAP_MOUSEOUT, + { + event, + element: hoveredElement, + datum: datum, + } + ); + + // Dispatch hide tooltip event + self.services.events.dispatchEvent(Events.Tooltip.HIDE, { + event, + hoveredElement, + }); + }); + } + + // Highlight elements that match the hovered axis item + handleAxisOnHover = (event: CustomEvent) => { + const { datum } = event.detail; + // Unique ranges and domains + const ranges = this.model.getUniqueRanges(); + const domains = this.model.getUniqueDomain(); + // Labels + const domainLabel = this.services.cartesianScales.getDomainLabel(); + const rangeLabel = this.services.cartesianScales.getRangeLabel(); + + let label = '', + sum = 0, + min = 0, + max = 0; + const ids = []; + + // Check to see where datum belongs + if (this.matrix[datum] != undefined) { + label = domainLabel; + // Iterate through Object and get sum, min, and max + ranges.forEach((element) => { + let value = this.matrix[datum][element].value || 0; + sum += value; + min = value < min ? value : min; + max = value > max ? value : max; + const id = this.matrix[datum][element].index; + if (id >= 0) { + ids.push(`.heat-${id}`); + } + }); + } else { + label = rangeLabel; + domains.forEach((element) => { + let value = this.matrix[element][datum].value || 0; + sum += value; + min = value < min ? value : min; + max = value > max ? value : max; + const id = this.matrix[element][datum].index; + + if (id >= 0) { + ids.push(`rect.heat-${id}`); + } + }); + } + + this.parent + .selectAll(ids.join(',')) + .classed('axis-hovered', true) + .style('stroke', 'white') + .style('stroke-width', '3px') + .raise(); + + // Dispatch tooltip show event + this.services.events.dispatchEvent(Events.Tooltip.SHOW, { + event, + hoveredElement: select(event.detail.element), + items: [ + { + label: label, + value: datum, + bold: true, + }, + { + label: 'Min', + value: min, + }, + { + label: 'Max', + value: max, + }, + { + label: 'Average', + value: sum / domains.length, + }, + ], + }); + }; + + // Un-highlight all elements + handleAxisMouseOut = (event: CustomEvent) => { + const dividerStatus = Tools.getProperty( + this.getOptions(), + 'heatmap', + 'divider', + 'state' + ); + + const hoveredRects = this.parent + .selectAll('rect.axis-hovered') + .classed('axis-hovered', false); + + if ( + (dividerStatus === DividerStatus.AUTO && + Configuration.heatmap.minCellDividerDimension <= + this.xBandwidth && + Configuration.heatmap.minCellDividerDimension <= + this.yBandwidth) || + dividerStatus === DividerStatus.ON + ) { + /** + * @question + * Use Gray 10 on white theme, but what about the others? + */ + hoveredRects + .style('stroke', '#f4f4f4') + .style('stroke-width', '1px'); + } else { + hoveredRects.style('stroke', 'none').style('stroke-width', '0px'); + } + + // Dispatch hide tooltip event + this.services.events.dispatchEvent(Events.Tooltip.HIDE, { + event, + }); + }; + + // Remove event listeners + destroy() { + this.parent + .selectAll('rect.heat') + .on('mouseover', null) + .on('mousemove', null) + .on('click', null) + .on('mouseout', null); + + // Remove legend listeners + const eventsFragment = this.services.events; + eventsFragment.removeEventListener( + Events.Legend.ITEM_HOVER, + this.handleAxisOnHover + ); + eventsFragment.removeEventListener( + Events.Legend.ITEM_MOUSEOUT, + this.handleAxisMouseOut + ); + } +} From 1f8bb9a987f24765e30e7d8866573e96537bb47b Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:07:55 -0400 Subject: [PATCH 05/68] Setup heatmap configs --- .../src/configuration-non-customizable.ts | 4 +++ packages/core/src/configuration.ts | 26 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/packages/core/src/configuration-non-customizable.ts b/packages/core/src/configuration-non-customizable.ts index aa197d7f3f..c29c9c980b 100644 --- a/packages/core/src/configuration-non-customizable.ts +++ b/packages/core/src/configuration-non-customizable.ts @@ -204,6 +204,10 @@ export const alluvial = { }, }; +export const heatmap = { + minCellDividerDimension: 16, +}; + export const spacers = { default: { size: 24, diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index 2acfd82592..a26096c694 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -45,6 +45,8 @@ import { ZoomBarTypes, LegendItemType, TreeTypes, + HeatmapChartOptions, + DividerStatus, } from './interfaces'; import enUSLocaleObject from 'date-fns/locale/en-US/index'; import { circlePack } from './configuration-non-customizable'; @@ -590,6 +592,29 @@ const alluvialChart: AlluvialChartOptions = Tools.merge({}, chart, { }, } as AlluvialChartOptions); +const heatmapChart: HeatmapChartOptions = Tools.merge({}, chart, { + axes, + timeScale, + // grid, + // ruler, + zoomBar: { + zoomRatio: 0.4, + minZoomRatio: 0.01, + top: { + enabled: false, + type: ZoomBarTypes.GRAPH_VIEW, + }, + } as ZoomBarsOptions, + heatmap: { + divider: { + state: DividerStatus.AUTO, + }, + colorPalette: { + type: 'purple', + }, + }, +} as HeatmapChartOptions); + export const options = { chart, axisChart, @@ -617,6 +642,7 @@ export const options = { circlePackChart, wordCloudChart, alluvialChart, + heatmapChart, }; export * from './configuration-non-customizable'; From 90b6aa3b2f8229c3006ff6155b42e4442fc2fa22 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:10:11 -0400 Subject: [PATCH 06/68] Export heatmap component --- packages/core/src/charts/index.ts | 1 + packages/core/src/components/index.ts | 2 ++ 2 files changed, 3 insertions(+) diff --git a/packages/core/src/charts/index.ts b/packages/core/src/charts/index.ts index dd37195335..9d85dd5084 100644 --- a/packages/core/src/charts/index.ts +++ b/packages/core/src/charts/index.ts @@ -21,3 +21,4 @@ export * from './treemap'; export * from './circle-pack'; export * from './wordcloud'; export * from './alluvial'; +export * from './heatmap'; diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 976e673608..c7e1b238f3 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -2,6 +2,7 @@ export * from './component'; // ESSENTIALS export * from './essentials/legend'; +export * from './essentials/color-scale-legend'; export * from './essentials/modal'; export * from './essentials/threshold'; export * from './essentials/title'; @@ -36,6 +37,7 @@ export * from './graphs/radar'; export * from './graphs/circle-pack'; export * from './graphs/wordcloud'; export * from './graphs/alluvial'; +export * from './graphs/heatmap'; // Layout export * from './layout/spacer'; From 3480b3fe86938eef7c7e2d8197a31b179bd382c2 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:10:38 -0400 Subject: [PATCH 07/68] Export heatmap data --- packages/core/demo/data/CHART_TYPES.ts | 5 + packages/core/demo/data/heatmap.ts | 1116 ++++++++++++++++++++++++ packages/core/demo/data/index.ts | 30 + 3 files changed, 1151 insertions(+) create mode 100644 packages/core/demo/data/heatmap.ts diff --git a/packages/core/demo/data/CHART_TYPES.ts b/packages/core/demo/data/CHART_TYPES.ts index bd9c95311f..038b000988 100644 --- a/packages/core/demo/data/CHART_TYPES.ts +++ b/packages/core/demo/data/CHART_TYPES.ts @@ -49,6 +49,11 @@ export default { angular: 'ibm-grouped-bar-chart', vue: 'ccv-grouped-bar-chart', }, + HeatmapChart: { + vanilla: 'HeatmapChart', + angular: 'ibm-heatmap-chart', + vue: 'ccv-heatmap-chart', + }, HistogramChart: { vanilla: 'HistogramChart', angular: 'ibm-histogram-chart', diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts new file mode 100644 index 0000000000..13ce11b5a6 --- /dev/null +++ b/packages/core/demo/data/heatmap.ts @@ -0,0 +1,1116 @@ +export const heatmapData = [ + { + letter: 'A', + month: 'January', + value: 41, + }, + { + letter: 'B', + month: 'January', + value: 7, + }, + { + letter: 'C', + month: 'January', + value: 66, + }, + { + letter: 'D', + month: 'January', + value: 85, + }, + { + letter: 'E', + month: 'January', + value: 70, + }, + { + letter: 'F', + month: 'January', + value: 98, + }, + { + letter: 'G', + month: 'January', + value: 90, + }, + { + letter: 'H', + month: 'January', + value: 66, + }, + { + letter: 'I', + month: 'January', + value: 0, + }, + { + letter: 'J', + month: 'January', + value: 13, + }, + { + letter: 'A', + month: 'February', + value: 16, + }, + { + letter: 'B', + month: 'February', + value: 5, + }, + { + letter: 'C', + month: 'February', + value: 6, + }, + { + letter: 'D', + month: 'February', + value: 48, + }, + { + letter: 'E', + month: 'February', + value: 72, + }, + { + letter: 'F', + month: 'February', + value: 26, + }, + { + letter: 'G', + month: 'February', + value: 70, + }, + { + letter: 'H', + month: 'February', + value: 99, + }, + { + letter: 'I', + month: 'February', + value: 79, + }, + { + letter: 'J', + month: 'February', + value: 83, + }, + { + letter: 'A', + month: 'March', + value: 62, + }, + { + letter: 'B', + month: 'March', + value: 57, + }, + { + letter: 'C', + month: 'March', + value: 90, + }, + { + letter: 'D', + month: 'March', + value: 68, + }, + { + letter: 'E', + month: 'March', + value: 84, + }, + { + letter: 'F', + month: 'March', + value: 21, + }, + { + letter: 'G', + month: 'March', + value: 54, + }, + { + letter: 'H', + month: 'March', + value: 25, + }, + { + letter: 'I', + month: 'March', + value: 42, + }, + { + letter: 'J', + month: 'March', + value: 62, + }, + { + letter: 'A', + month: 'April', + value: 15, + }, + { + letter: 'B', + month: 'April', + value: 52, + }, + { + letter: 'C', + month: 'April', + value: 15, + }, + { + letter: 'D', + month: 'April', + value: 22, + }, + { + letter: 'E', + month: 'April', + value: 59, + }, + { + letter: 'F', + month: 'April', + value: 36, + }, + { + letter: 'G', + month: 'April', + value: 5, + }, + { + letter: 'H', + month: 'April', + value: 18, + }, + { + letter: 'I', + month: 'April', + value: 42, + }, + { + letter: 'J', + month: 'April', + value: 72, + }, + { + letter: 'A', + month: 'May', + value: 30, + }, + { + letter: 'B', + month: 'May', + value: 39, + }, + { + letter: 'C', + month: 'May', + value: 69, + }, + { + letter: 'D', + month: 'May', + value: 73, + }, + { + letter: 'E', + month: 'May', + value: 2, + }, + { + letter: 'F', + month: 'May', + value: 15, + }, + { + letter: 'G', + month: 'May', + value: 86, + }, + { + letter: 'H', + month: 'May', + value: 23, + }, + { + letter: 'I', + month: 'May', + value: 65, + }, + { + letter: 'J', + month: 'May', + value: 0, + }, + { + letter: 'A', + month: 'June', + value: 51, + }, + { + letter: 'B', + month: 'June', + value: 30, + }, + { + letter: 'C', + month: 'June', + value: 7, + }, + { + letter: 'D', + month: 'June', + value: 74, + }, + { + letter: 'E', + month: 'June', + value: 44, + }, + { + letter: 'F', + month: 'June', + value: 62, + }, + { + letter: 'G', + month: 'June', + value: 65, + }, + { + letter: 'H', + month: 'June', + value: 35, + }, + { + letter: 'I', + month: 'June', + value: 95, + }, + { + letter: 'J', + month: 'June', + value: 59, + }, + { + letter: 'A', + month: 'July', + value: 89, + }, + { + letter: 'B', + month: 'July', + value: 50, + }, + { + letter: 'C', + month: 'July', + value: 35, + }, + { + letter: 'D', + month: 'July', + value: 45, + }, + { + letter: 'E', + month: 'July', + value: 93, + }, + { + letter: 'F', + month: 'July', + value: 19, + }, + { + letter: 'G', + month: 'July', + value: 52, + }, + { + letter: 'H', + month: 'July', + value: 81, + }, + { + letter: 'I', + month: 'July', + value: 72, + }, + { + letter: 'J', + month: 'July', + value: 99, + }, + { + letter: 'A', + month: 'August', + value: 54, + }, + { + letter: 'B', + month: 'August', + value: 41, + }, + { + letter: 'C', + month: 'August', + value: 75, + }, + { + letter: 'D', + month: 'August', + value: 10, + }, + { + letter: 'E', + month: 'August', + value: 0, + }, + { + letter: 'F', + month: 'August', + value: 93, + }, + { + letter: 'G', + month: 'August', + value: 3, + }, + { + letter: 'H', + month: 'August', + value: 80, + }, + { + letter: 'I', + month: 'August', + value: 88, + }, + { + letter: 'J', + month: 'August', + value: 27, + }, + { + letter: 'A', + month: 'September', + value: 81, + }, + { + letter: 'B', + month: 'September', + value: 36, + }, + { + letter: 'C', + month: 'September', + value: 77, + }, + { + letter: 'D', + month: 'September', + value: 1, + }, + { + letter: 'E', + month: 'September', + value: 45, + }, + { + letter: 'F', + month: 'September', + value: 23, + }, + { + letter: 'G', + month: 'September', + value: 1, + }, + { + letter: 'H', + month: 'September', + value: 13, + }, + { + letter: 'I', + month: 'September', + value: 61, + }, + { + letter: 'J', + month: 'September', + value: 87, + }, + { + letter: 'A', + month: 'October', + value: 5, + }, + { + letter: 'B', + month: 'October', + value: 29, + }, + { + letter: 'C', + month: 'October', + value: 49, + }, + { + letter: 'D', + month: 'October', + value: 81, + }, + { + letter: 'E', + month: 'October', + value: 5, + }, + { + letter: 'F', + month: 'October', + value: 6, + }, + { + letter: 'G', + month: 'October', + value: 3, + }, + { + letter: 'H', + month: 'October', + value: 72, + }, + { + letter: 'I', + month: 'October', + value: 27, + }, + { + letter: 'J', + month: 'October', + value: 99, + }, + { + letter: 'A', + month: 'November', + value: 25, + }, + { + letter: 'B', + month: 'November', + value: 11, + }, + { + letter: 'C', + month: 'November', + value: 54, + }, + { + letter: 'D', + month: 'November', + value: 90, + }, + { + letter: 'E', + month: 'November', + value: 21, + }, + { + letter: 'F', + month: 'November', + value: 5, + }, + { + letter: 'G', + month: 'November', + value: 41, + }, + { + letter: 'H', + month: 'November', + value: 4, + }, + { + letter: 'I', + month: 'November', + value: 31, + }, + { + letter: 'J', + month: 'November', + value: 22, + }, + { + letter: 'A', + month: 'December', + value: 99, + }, + { + letter: 'B', + month: 'December', + value: 54, + }, + { + letter: 'C', + month: 'December', + value: 85, + }, + { + letter: 'D', + month: 'December', + value: 39, + }, + { + letter: 'E', + month: 'December', + value: 45, + }, + { + letter: 'F', + month: 'December', + value: 24, + }, + { + letter: 'G', + month: 'December', + value: 87, + }, + { + letter: 'H', + month: 'December', + value: 69, + }, + { + letter: 'I', + month: 'December', + value: 59, + }, + { + letter: 'J', + month: 'December', + value: 44, + }, +]; + +export const heatmapOptions = { + title: 'Heatmap', + axes: { + bottom: { + title: 'Letters', + mapsTo: 'letter', + scaleType: 'labels', + }, + left: { + title: 'Months', + mapsTo: 'month', + scaleType: 'labels', + }, + }, +}; + +export const heatmapColorOptions = { + title: 'Heatmap (Color options)', + heatmap: { + colorPalette: { + type: 'teal', + }, + }, + axes: { + bottom: { + title: 'Letters', + mapsTo: 'letter', + scaleType: 'labels', + }, + left: { + title: 'Months', + mapsTo: 'month', + scaleType: 'labels', + }, + }, +}; + +export const heatmapMissingData = [ + { + letter: 'A', + month: 'January', + value: 41, + }, + { + letter: 'B', + month: 'January', + value: 7, + }, + { + letter: 'C', + month: 'January', + value: 66, + }, + { + letter: 'D', + month: 'January', + value: 85, + }, + { + letter: 'E', + month: 'January', + value: 70, + }, + { + letter: 'F', + month: 'January', + value: 98, + }, + { + letter: 'G', + month: 'January', + value: 90, + }, + { + letter: 'H', + month: 'January', + value: 66, + }, + { + letter: 'I', + month: 'January', + value: 0, + }, + { + letter: 'J', + month: 'January', + value: 13, + }, + { + letter: 'A', + month: 'February', + value: 16, + }, + { + letter: 'B', + month: 'February', + value: 5, + }, + { + letter: 'C', + month: 'February', + value: 6, + }, + { + letter: 'D', + month: 'February', + value: 48, + }, + { + letter: 'J', + month: 'February', + value: 83, + }, + { + letter: 'A', + month: 'March', + value: 62, + }, + { + letter: 'B', + month: 'March', + value: 57, + }, + { + letter: 'C', + month: 'March', + value: 90, + }, + { + letter: 'D', + month: 'March', + value: 68, + }, + { + letter: 'E', + month: 'March', + value: 84, + }, + { + letter: 'F', + month: 'March', + value: 21, + }, + { + letter: 'I', + month: 'March', + value: 42, + }, + { + letter: 'A', + month: 'April', + value: 15, + }, + { + letter: 'B', + month: 'April', + value: 52, + }, + { + letter: 'D', + month: 'April', + value: 22, + }, + { + letter: 'E', + month: 'April', + value: 59, + }, + { + letter: 'G', + month: 'April', + value: 5, + }, + { + letter: 'I', + month: 'April', + value: 42, + }, + { + letter: 'J', + month: 'April', + value: 72, + }, + { + letter: 'B', + month: 'May', + value: 39, + }, + { + letter: 'C', + month: 'May', + value: 69, + }, + { + letter: 'E', + month: 'May', + value: 2, + }, + { + letter: 'F', + month: 'May', + value: 15, + }, + { + letter: 'H', + month: 'May', + value: 23, + }, + { + letter: 'I', + month: 'May', + value: 65, + }, + { + letter: 'A', + month: 'June', + value: 51, + }, + { + letter: 'B', + month: 'June', + value: 30, + }, + { + letter: 'I', + month: 'June', + value: 95, + }, + { + letter: 'J', + month: 'June', + value: 59, + }, + { + letter: 'A', + month: 'July', + value: 89, + }, + { + letter: 'B', + month: 'July', + value: 50, + }, + { + letter: 'C', + month: 'July', + value: 35, + }, + { + letter: 'D', + month: 'July', + value: 45, + }, + { + letter: 'E', + month: 'July', + value: 93, + }, + { + letter: 'F', + month: 'July', + value: 19, + }, + { + letter: 'G', + month: 'July', + value: 52, + }, + { + letter: 'H', + month: 'July', + value: 81, + }, + { + letter: 'I', + month: 'July', + value: 72, + }, + { + letter: 'J', + month: 'July', + value: 99, + }, + { + letter: 'A', + month: 'August', + value: 54, + }, + { + letter: 'D', + month: 'August', + value: 10, + }, + { + letter: 'E', + month: 'August', + value: 0, + }, + { + letter: 'F', + month: 'August', + value: 93, + }, + { + letter: 'G', + month: 'August', + value: 3, + }, + { + letter: 'H', + month: 'August', + value: 80, + }, + { + letter: 'I', + month: 'August', + value: 88, + }, + { + letter: 'J', + month: 'August', + value: 27, + }, + { + letter: 'B', + month: 'September', + value: 36, + }, + { + letter: 'C', + month: 'September', + value: 77, + }, + { + letter: 'D', + month: 'September', + value: 1, + }, + { + letter: 'E', + month: 'September', + value: 45, + }, + { + letter: 'F', + month: 'September', + value: 23, + }, + { + letter: 'G', + month: 'September', + value: 1, + }, + { + letter: 'H', + month: 'September', + value: 13, + }, + { + letter: 'I', + month: 'September', + value: 61, + }, + { + letter: 'J', + month: 'September', + value: 87, + }, + { + letter: 'A', + month: 'October', + value: 5, + }, + { + letter: 'B', + month: 'October', + value: 29, + }, + { + letter: 'C', + month: 'October', + value: 49, + }, + { + letter: 'D', + month: 'October', + value: 81, + }, + { + letter: 'E', + month: 'October', + value: 5, + }, + { + letter: 'F', + month: 'October', + value: 6, + }, + { + letter: 'J', + month: 'October', + value: 99, + }, + { + letter: 'A', + month: 'November', + value: 25, + }, + { + letter: 'B', + month: 'November', + value: 11, + }, + { + letter: 'C', + month: 'November', + value: 54, + }, + { + letter: 'F', + month: 'November', + value: 5, + }, + { + letter: 'G', + month: 'November', + value: 41, + }, + { + letter: 'H', + month: 'November', + value: 4, + }, + { + letter: 'I', + month: 'November', + value: 31, + }, + { + letter: 'J', + month: 'November', + value: 22, + }, + { + letter: 'A', + month: 'December', + value: 99, + }, + { + letter: 'B', + month: 'December', + value: 54, + }, + { + letter: 'C', + month: 'December', + value: 85, + }, + { + letter: 'D', + month: 'December', + value: 39, + }, + { + letter: 'E', + month: 'December', + value: 45, + }, + { + letter: 'F', + month: 'December', + value: 24, + }, + { + letter: 'G', + month: 'December', + value: 87, + }, +]; + +export const heatmapMissingDataOptions = { + title: 'Heatmap (Missing data)', + axes: { + bottom: { + title: 'Letters', + mapsTo: 'letter', + scaleType: 'labels', + }, + left: { + title: 'Months', + mapsTo: 'month', + scaleType: 'labels', + }, + }, +}; + +export const heatmapTimescaleOptions = { + title: 'Heatmap (timescale)', + axes: { + left: { + title: 'Letter', + mapsTo: 't', + scaleType: 'labels', + }, + bottom: { + title: 'Time', + mapsTo: 'time', + scaleType: 'time', + }, + }, +}; + +export const heatmapTimeScaleData = []; diff --git a/packages/core/demo/data/index.ts b/packages/core/demo/data/index.ts index d6a8e89a45..afb2659c1d 100644 --- a/packages/core/demo/data/index.ts +++ b/packages/core/demo/data/index.ts @@ -24,6 +24,7 @@ import * as zoomBarDemos from './zoom-bar'; import * as highScaleDemos from './high-scale'; import * as alluvialDemos from './alluvial'; import * as highlightDemos from './hightlight'; +import * as heatmapDemos from './heatmap'; export * from './area'; export * from './bar'; @@ -49,6 +50,7 @@ export * from './wordcloud'; export * from './zoom-bar'; export * from './high-scale'; export * from './alluvial'; +export * from './heatmap'; import { createChartSandbox, @@ -801,6 +803,34 @@ const simpleChartDemos = [ }, ], }, + { + title: 'Heatmap', + configs: { + excludeColorPaletteControl: true, + }, + demos: [ + { + options: heatmapDemos.heatmapOptions, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapColorOptions, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapMissingDataOptions, + data: heatmapDemos.heatmapMissingData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapTimescaleOptions, + data: heatmapDemos.heatmapTimeScaleData, + chartType: chartTypes.HeatmapChart, + }, + ], + }, { title: 'Histogram', demos: [ From f30d9222ed457a3576bb477793740c290680ced2 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:12:09 -0400 Subject: [PATCH 08/68] Update cartesian model inheritance --- packages/core/src/model/cartesian-charts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/model/cartesian-charts.ts b/packages/core/src/model/cartesian-charts.ts index 35cef3651a..92a46551fb 100644 --- a/packages/core/src/model/cartesian-charts.ts +++ b/packages/core/src/model/cartesian-charts.ts @@ -16,7 +16,7 @@ export class ChartModelCartesian extends ChartModel { // get the scales information // needed for getTabularArray() - private assignRangeAndDomains() { + protected assignRangeAndDomains() { const { cartesianScales } = this.services; const options = this.getOptions(); const isDualAxes = cartesianScales.isDualAxes(); From 58f02a7cc4f3a2ed461ccfd4969d144365eb73a7 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:13:05 -0400 Subject: [PATCH 09/68] Update axis chart to use color scale legend if heatmap (temp) --- packages/core/src/axis-chart.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/packages/core/src/axis-chart.ts b/packages/core/src/axis-chart.ts index 05df40f358..e0a53bbcd6 100644 --- a/packages/core/src/axis-chart.ts +++ b/packages/core/src/axis-chart.ts @@ -17,6 +17,7 @@ import { ChartClip, Modal, LayoutComponent, + ColorScaleLegend, Legend, Threshold, Highlight, @@ -47,6 +48,15 @@ export class AxisChart extends Chart { configs?: any ) { const options = this.model.getOptions(); + + /** + * @question + * Probably not the best place to put this as it'll be called in other charts + * Probbaly a good idea to create a similar class (almost exact copy - extending won't work) + * with heatmap specific options and layout? + */ + const isHeatmapEnabled = Tools.getProperty(options, 'heatmap'); + const isZoomBarEnabled = Tools.getProperty( options, 'zoomBar', @@ -112,10 +122,16 @@ export class AxisChart extends Chart { const legendComponent = { id: 'legend', - components: [new Legend(this.model, this.services)], + components: [ + isHeatmapEnabled + ? new ColorScaleLegend(this.model, this.services) + : new Legend(this.model, this.services), + ], growth: LayoutGrowth.PREFERRED, }; + // Check to see if it is heatmap here and call the color scale legend + // if all zoom bars are locked, no need to add chart brush if (zoomBarEnabled && !isZoomBarLocked) { graphFrameComponents.push( From f79640bac896152441e4feab68b830e2ea4677fa Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:15:21 -0400 Subject: [PATCH 10/68] Enable heatmap in angular --- packages/angular/src/charts.module.ts | 3 ++ .../angular/src/heatmap-chart.component.ts | 34 +++++++++++++++++++ packages/angular/src/index.ts | 1 + 3 files changed, 38 insertions(+) create mode 100644 packages/angular/src/heatmap-chart.component.ts diff --git a/packages/angular/src/charts.module.ts b/packages/angular/src/charts.module.ts index 79801e40a5..c364154dc9 100644 --- a/packages/angular/src/charts.module.ts +++ b/packages/angular/src/charts.module.ts @@ -25,6 +25,7 @@ import { TreeChartComponent } from './tree-chart.component'; import { TreemapChartComponent } from './treemap-chart.component'; import { CirclePackChartComponent } from './circle-pack-chart.component'; import { WordCloudChartComponent } from './wordcloud-chart.component'; +import { HeatmapChartComponent } from './heatmap-chart.component'; @NgModule({ imports: [CommonModule], @@ -41,6 +42,7 @@ import { WordCloudChartComponent } from './wordcloud-chart.component'; BulletChartComponent, DonutChartComponent, GaugeChartComponent, + HeatmapChartComponent, HistogramChartComponent, LineChartComponent, LollipopChartComponent, @@ -67,6 +69,7 @@ import { WordCloudChartComponent } from './wordcloud-chart.component'; BulletChartComponent, DonutChartComponent, GaugeChartComponent, + HeatmapChartComponent, HistogramChartComponent, LineChartComponent, LollipopChartComponent, diff --git a/packages/angular/src/heatmap-chart.component.ts b/packages/angular/src/heatmap-chart.component.ts new file mode 100644 index 0000000000..d279573f53 --- /dev/null +++ b/packages/angular/src/heatmap-chart.component.ts @@ -0,0 +1,34 @@ +import { + Component, + AfterViewInit +} from "@angular/core"; + +import { BaseChart } from "./base-chart.component"; + +import { HeatmapChart } from "@carbon/charts"; + +/** + * Wrapper around `Heatmap` in carbon charts library + * + * Most functions just call their equivalent from the chart library. + */ +@Component({ + selector: "ibm-heatmap-chart", + template: `` +}) +export class HeatmapChartComponent extends BaseChart implements AfterViewInit { + /** + * Runs after view init to create a chart, attach it to `elementRef` and draw it. + */ + ngAfterViewInit() { + this.chart = new HeatmapChart( + this.elementRef.nativeElement, + { + data: this.data, + options: this.options + } + ); + + Object.assign(this, this.chart); + } +} diff --git a/packages/angular/src/index.ts b/packages/angular/src/index.ts index 1808eb7579..4e2fad006b 100644 --- a/packages/angular/src/index.ts +++ b/packages/angular/src/index.ts @@ -24,6 +24,7 @@ export * from './treemap-chart.component'; export * from './circle-pack-chart.component'; export * from './wordcloud-chart.component'; export * from './alluvial-chart.component'; +export * from './heatmap-chart.component'; // Diagrams export * from './diagrams/card-node/card-node.module'; From 559639ad17c64c99cebc414ae99fe86aba52a9dc Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:15:41 -0400 Subject: [PATCH 11/68] Enable heatmap in react --- packages/react/src/heatmap-chart.tsx | 27 +++++++++++++++++++++++++++ packages/react/src/index.ts | 2 ++ 2 files changed, 29 insertions(+) create mode 100644 packages/react/src/heatmap-chart.tsx diff --git a/packages/react/src/heatmap-chart.tsx b/packages/react/src/heatmap-chart.tsx new file mode 100644 index 0000000000..e2b90d8a9e --- /dev/null +++ b/packages/react/src/heatmap-chart.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import { HeatmapChart as HMC } from '@carbon/charts'; +import BaseChart from './base-chart'; +import { ChartConfig, HeatmapChartOptions } from '@carbon/charts/interfaces'; + +type HeatmapChartProps = ChartConfig; + +export default class HeatmapChart extends BaseChart { + chartRef!: HTMLDivElement; + props!: HeatmapChartProps; + chart!: HMC; + + componentDidMount() { + this.chart = new HMC(this.chartRef, { + data: this.props.data, + options: this.props.options, + }); + } + + render() { + return ( +
(this.chartRef = chartRef!)} + className="chart-holder">
+ ); + } +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index 000c8b8379..670b981e05 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -21,6 +21,7 @@ import TreemapChart from './treemap-chart'; import CirclePackChart from './circle-pack-chart'; import WordCloudChart from './wordcloud-chart'; import AlluvialChart from './alluvial-chart'; +import HeatmapChart from './heatmap-chart'; export { AreaChart, @@ -46,4 +47,5 @@ export { CirclePackChart, WordCloudChart, AlluvialChart, + HeatmapChart, }; From 7407ca02ea48440b5084d305f2a7fa126ea5f56a Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:16:00 -0400 Subject: [PATCH 12/68] Enable heatmap in vue --- packages/vue/src/ccv-heatmap-chart.vue | 19 +++++++++++++++++++ packages/vue/src/index.js | 3 +++ 2 files changed, 22 insertions(+) create mode 100644 packages/vue/src/ccv-heatmap-chart.vue diff --git a/packages/vue/src/ccv-heatmap-chart.vue b/packages/vue/src/ccv-heatmap-chart.vue new file mode 100644 index 0000000000..f2b3c96d5a --- /dev/null +++ b/packages/vue/src/ccv-heatmap-chart.vue @@ -0,0 +1,19 @@ + + + diff --git a/packages/vue/src/index.js b/packages/vue/src/index.js index c3304a6a23..e279340051 100644 --- a/packages/vue/src/index.js +++ b/packages/vue/src/index.js @@ -21,6 +21,7 @@ import CcvTreemapChart from './ccv-treemap-chart.vue'; import CcvCirclePackChart from './ccv-circle-pack-chart.vue'; import CcvWordCloudChart from './ccv-wordcloud-chart.vue'; import CcvAlluvialChart from './ccv-alluvial-chart.vue'; +import CcvHeatmapChart from './ccv-heatmap-chart.vue'; const components = [ CcvAreaChart, @@ -46,6 +47,7 @@ const components = [ CcvCirclePackChart, CcvWordCloudChart, CcvAlluvialChart, + CcvHeatmapChart, ]; /* @@ -104,4 +106,5 @@ export { CcvCirclePackChart, CcvWordCloudChart, CcvAlluvialChart, + CcvHeatmapChart, }; From 9f6893c1970d57490fb40e33020bb81df7e3f994 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 25 Oct 2021 10:16:17 -0400 Subject: [PATCH 13/68] Enable heatmap in svelte --- packages/svelte/src/HeatmapChart.svelte | 16 ++++++++++++++++ packages/svelte/src/index.js | 2 ++ 2 files changed, 18 insertions(+) create mode 100644 packages/svelte/src/HeatmapChart.svelte diff --git a/packages/svelte/src/HeatmapChart.svelte b/packages/svelte/src/HeatmapChart.svelte new file mode 100644 index 0000000000..9e9be0d9e7 --- /dev/null +++ b/packages/svelte/src/HeatmapChart.svelte @@ -0,0 +1,16 @@ + + + diff --git a/packages/svelte/src/index.js b/packages/svelte/src/index.js index cd7f9b91f1..eedc618760 100644 --- a/packages/svelte/src/index.js +++ b/packages/svelte/src/index.js @@ -21,6 +21,7 @@ import TreemapChart from './TreemapChart.svelte'; import CirclePackChart from './CirclePackChart.svelte'; import WordCloudChart from './WordCloudChart.svelte'; import AlluvialChart from './AlluvialChart.svelte'; +import HeatmapChart from './HeatmapChart.svelte'; export { AreaChart, @@ -46,4 +47,5 @@ export { CirclePackChart, WordCloudChart, AlluvialChart, + HeatmapChart, }; From 5e948595af9da25acc9e574ba5890944aa2cc9ab Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 26 Oct 2021 02:05:04 -0400 Subject: [PATCH 14/68] Reposition tooltip on axes hover --- packages/core/src/components/graphs/heatmap.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index f63270cd66..ff022dd296 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -270,7 +270,8 @@ export class Heatmap extends Component { // Highlight elements that match the hovered axis item handleAxisOnHover = (event: CustomEvent) => { - const { datum } = event.detail; + const { detail } = event; + const { datum } = detail; // Unique ranges and domains const ranges = this.model.getUniqueRanges(); const domains = this.model.getUniqueDomain(); @@ -322,7 +323,7 @@ export class Heatmap extends Component { // Dispatch tooltip show event this.services.events.dispatchEvent(Events.Tooltip.SHOW, { - event, + event: detail.event, hoveredElement: select(event.detail.element), items: [ { From 7e49630df3600d4cdd62a948e6efa17202fd8cd7 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 26 Oct 2021 02:05:42 -0400 Subject: [PATCH 15/68] Remove time axes demo --- packages/core/demo/data/heatmap.ts | 18 ------------------ packages/core/demo/data/index.ts | 5 ----- 2 files changed, 23 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index 13ce11b5a6..53800b65e1 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -1096,21 +1096,3 @@ export const heatmapMissingDataOptions = { }, }, }; - -export const heatmapTimescaleOptions = { - title: 'Heatmap (timescale)', - axes: { - left: { - title: 'Letter', - mapsTo: 't', - scaleType: 'labels', - }, - bottom: { - title: 'Time', - mapsTo: 'time', - scaleType: 'time', - }, - }, -}; - -export const heatmapTimeScaleData = []; diff --git a/packages/core/demo/data/index.ts b/packages/core/demo/data/index.ts index afb2659c1d..631e94cbca 100644 --- a/packages/core/demo/data/index.ts +++ b/packages/core/demo/data/index.ts @@ -824,11 +824,6 @@ const simpleChartDemos = [ data: heatmapDemos.heatmapMissingData, chartType: chartTypes.HeatmapChart, }, - { - options: heatmapDemos.heatmapTimescaleOptions, - data: heatmapDemos.heatmapTimeScaleData, - chartType: chartTypes.HeatmapChart, - }, ], }, { From 7e598141bd50881af484b8cfad5b98902cd44751 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 27 Oct 2021 01:02:21 -0400 Subject: [PATCH 16/68] Remove hardcoded styles on hover --- packages/core/src/components/graphs/heatmap.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index ff022dd296..deafc08852 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -86,11 +86,7 @@ export class Heatmap extends Component { .style('fill', (d) => { // Check if a valid value exists if (d.index === -1 || d.value === null) { - /** - * @question - Determine what colors should be displaced based on theme for empty cells - * Class null-state, can be used to assign fill color in scss - */ - return '#f4f4f4'; + return null; } return this.model.getFillColor(Number(d.value)); }) @@ -170,10 +166,7 @@ export class Heatmap extends Component { // Highlight element hoveredElement .raise() - .classed('raised', true) - .style('stroke', 'white') - .style('stroke-width', '3px'); - + .classed('raised', true); // Dispatch tooltip show event self.services.events.dispatchEvent(Events.Tooltip.SHOW, { event, @@ -318,7 +311,7 @@ export class Heatmap extends Component { .selectAll(ids.join(',')) .classed('axis-hovered', true) .style('stroke', 'white') - .style('stroke-width', '3px') + .style('stroke-width', '2px') .raise(); // Dispatch tooltip show event From 2a631bd8df7db74a07e0a48b0f30964a6e5be1a1 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 27 Oct 2021 01:03:55 -0400 Subject: [PATCH 17/68] Create heatmap styles --- packages/core/src/styles/graphs/_heatmap.scss | 30 +++++++++++++++++++ packages/core/src/styles/graphs/index.scss | 1 + 2 files changed, 31 insertions(+) create mode 100644 packages/core/src/styles/graphs/_heatmap.scss diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss new file mode 100644 index 0000000000..1c8f25a51c --- /dev/null +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -0,0 +1,30 @@ +.#{$prefix}--#{$charts-prefix}--heatmap { + + rect.raised { + filter: drop-shadow(0px 0px 8px black); + } + + @if $carbon--theme == $carbon--theme--white { + rect.null-state { + fill: #f4f4f4; + } + } + + @if $carbon--theme == $carbon--theme--g10 { + rect.null-state { + fill: #ffffff; + } + } + + @if $carbon--theme == $carbon--theme--g90 { + rect.null-state { + fill: #161616; + } + } + + @if $carbon--theme == $carbon--theme--g100 { + rect.null-state { + fill: #262626; + } + } +} diff --git a/packages/core/src/styles/graphs/index.scss b/packages/core/src/styles/graphs/index.scss index f3d12bc177..7aba4891e6 100644 --- a/packages/core/src/styles/graphs/index.scss +++ b/packages/core/src/styles/graphs/index.scss @@ -15,3 +15,4 @@ @import './circle-pack'; @import './wordcloud'; @import './alluvial'; +@import './heatmap'; From 251e6948b5370742726f48023e0ff5c574a0a3a2 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 27 Oct 2021 01:05:49 -0400 Subject: [PATCH 18/68] Format files --- packages/core/src/components/graphs/heatmap.ts | 4 +--- packages/core/src/styles/graphs/_heatmap.scss | 1 - 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index deafc08852..28cade00f2 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -164,9 +164,7 @@ export class Heatmap extends Component { // Perform visual changes only if value is valid // Highlight element - hoveredElement - .raise() - .classed('raised', true); + hoveredElement.raise().classed('raised', true); // Dispatch tooltip show event self.services.events.dispatchEvent(Events.Tooltip.SHOW, { event, diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 1c8f25a51c..01adff3f0f 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -1,5 +1,4 @@ .#{$prefix}--#{$charts-prefix}--heatmap { - rect.raised { filter: drop-shadow(0px 0px 8px black); } From fde42f2aea224ecf2f5fb5b8c5eeac5aa22cbc68 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 9 Nov 2021 02:12:21 -0500 Subject: [PATCH 19/68] enable multiple color legend options --- packages/core/src/configuration.ts | 16 ++++------- packages/core/src/interfaces/components.ts | 8 ++++++ packages/core/src/interfaces/enums.ts | 8 ++++++ packages/core/src/model/heatmap.ts | 33 ++++++++++++---------- 4 files changed, 39 insertions(+), 26 deletions(-) diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index a26096c694..8275eb7fb0 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -594,17 +594,6 @@ const alluvialChart: AlluvialChartOptions = Tools.merge({}, chart, { const heatmapChart: HeatmapChartOptions = Tools.merge({}, chart, { axes, - timeScale, - // grid, - // ruler, - zoomBar: { - zoomRatio: 0.4, - minZoomRatio: 0.01, - top: { - enabled: false, - type: ZoomBarTypes.GRAPH_VIEW, - }, - } as ZoomBarsOptions, heatmap: { divider: { state: DividerStatus.AUTO, @@ -613,6 +602,11 @@ const heatmapChart: HeatmapChartOptions = Tools.merge({}, chart, { type: 'purple', }, }, + legend: { + colorLegend: { + type: 'linear', + }, + }, } as HeatmapChartOptions); export const options = { diff --git a/packages/core/src/interfaces/components.ts b/packages/core/src/interfaces/components.ts index a7c1e15f86..23f069e053 100644 --- a/packages/core/src/interfaces/components.ts +++ b/packages/core/src/interfaces/components.ts @@ -4,6 +4,7 @@ import { Alignments, ToolbarControlTypes, ZoomBarTypes, + ColorLegendType } from './enums'; import { Component } from '../components/component'; import { TruncationOptions } from './truncation'; @@ -44,6 +45,13 @@ export interface LegendOptions { * customized legend items */ additionalItems?: LegendItem[]; + /** + * customize color legend + * enabled by default on select charts + */ + colorLegend?: { + type: ColorLegendType; + } } /** diff --git a/packages/core/src/interfaces/enums.ts b/packages/core/src/interfaces/enums.ts index e42a85b324..36e235461d 100644 --- a/packages/core/src/interfaces/enums.ts +++ b/packages/core/src/interfaces/enums.ts @@ -251,6 +251,14 @@ export enum LegendItemType { ZOOM = 'zoom', } +/** + * enum of color legend types + */ +export enum ColorLegendType { + LINEAR = 'linear', + QUANTILE = 'quantile', +} + /** * enum of axis ticks rotation */ diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 5655c25afd..e674a6071f 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -8,7 +8,7 @@ import { format } from 'date-fns'; // d3 imports import { extent } from 'd3-array'; -import { scaleLinear } from 'd3-scale'; +import { scaleLinear, scaleQuantize } from 'd3-scale'; /** The gauge chart model layer */ export class HeatmapModel extends ChartModelCartesian { @@ -159,7 +159,19 @@ export class HeatmapModel extends ChartModelCartesian { domain.push(value); }); - return domain; + const options = this.getOptions(); + if ( + Tools.getProperty(options, 'legend', 'colorLegend', 'type') === + 'linear' + ) { + return domain; + } + + /** + * @todo + * Clean this up! + */ + return domain.length !== 2 ? [0, 1] : [0, 100]; } /** @@ -181,19 +193,10 @@ export class HeatmapModel extends ChartModelCartesian { */ getColorScale() { if (!this._colorScale) { - this._colorScale = scaleLinear() - /** - * @todo - * If getpalette() returns array of size 2 - * Check to see if, there is negative numbers and positive numbers - * If Yes, then use min and max values - * - * OR - * - * IN getTicks, compare length of `colors` and `ticks(return)` variable. - * If they do not match, use min and max, and the first and last color. - */ - .domain(this.getTicks()) + this._colorScale = scaleQuantize() + .domain(this.getValueDomain() as [number, number]) + // scaleLinear() + // .domain(this.getTicks()) .range(this.getPalettes()); } From 3a9d14df996886ed9b6cbc2d6c5485ee194366fd Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 9 Nov 2021 02:13:46 -0500 Subject: [PATCH 20/68] Add check for scaleTypes that are not labels --- packages/core/src/model/heatmap.ts | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index e674a6071f..a77746acaf 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -123,6 +123,31 @@ export class HeatmapModel extends ChartModelCartesian { constructor(services: any) { super(services); + + // Check which scale types are being used + const axis = Tools.getProperty(this.getOptions(), 'axes'); + + // Need to check options since scale service hasn't been instantiated + if ( + (!!Tools.getProperty(axis, 'left', 'scaleType') && + Tools.getProperty(axis, 'left', 'scaleType') !== + ScaleTypes.LABELS) || + (!!Tools.getProperty(axis, 'right', 'scaleType') && + Tools.getProperty(axis, 'right', 'scaleType') !== + ScaleTypes.LABELS) || + (!!Tools.getProperty(axis, 'top', 'scaleType') && + Tools.getProperty(axis, 'top', 'scaleType') !== + ScaleTypes.LABELS) || + !!Tools.getProperty( + axis, + 'bottom', + 'scaleType' && + Tools.getProperty(axis, 'bottom', 'scaleType') !== + ScaleTypes.LABELS + ) + ) { + throw Error('Heatmap only supports label scaletypes.'); + } } getLinearScale() { From ec4bf894472072ccc1bbea1a5a7a2ce8eec77aba Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 9 Nov 2021 02:41:12 -0500 Subject: [PATCH 21/68] Improve get unique values performance conditionally --- packages/core/src/model/heatmap.ts | 26 +++++++++++++++++-- .../core/src/services/scales-cartesian.ts | 9 +++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index a77746acaf..37ebea6387 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -235,7 +235,18 @@ export class HeatmapModel extends ChartModelCartesian { getUniqueDomain(): string[] { if (Tools.isEmpty(this._domains)) { const displayData = this.getDisplayData(); - const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); + const { cartesianScales } = this.services; + + const domainIdentifier = cartesianScales.getDomainIdentifier(); + const mainXAxisPosition = cartesianScales.getMainXAxisPosition(); + const customDomain = cartesianScales.getCustomDomainValuesByposition( + mainXAxisPosition + ); + + // Use user defined domain if specified + if (!!customDomain) { + return customDomain; + } // Get unique axis values & create a matrix this._domains = Array.from( @@ -257,7 +268,18 @@ export class HeatmapModel extends ChartModelCartesian { getUniqueRanges(): string[] { if (Tools.isEmpty(this._range)) { const displayData = this.getDisplayData(); - const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); + const { cartesianScales } = this.services; + + const rangeIdentifier = cartesianScales.getRangeIdentifier(); + const mainYAxisPosition = cartesianScales.getMainYAxisPosition(); + const customDomain = cartesianScales.getCustomDomainValuesByposition( + mainYAxisPosition + ); + + // Use user defined domain if specified + if (!!customDomain) { + return customDomain; + } // Get unique axis values & create a matrix this._range = Array.from( diff --git a/packages/core/src/services/scales-cartesian.ts b/packages/core/src/services/scales-cartesian.ts index e6836bafba..aea8b19461 100644 --- a/packages/core/src/services/scales-cartesian.ts +++ b/packages/core/src/services/scales-cartesian.ts @@ -214,6 +214,15 @@ export class CartesianScales extends Service { } } + getCustomDomainValuesByposition(axisPosition: AxisPositions) { + return Tools.getProperty( + this.model.getOptions(), + 'axes', + axisPosition, + 'domain' + ); + } + getOrientation() { return this.orientation; } From cd855a3c969bdc3f17850478274ff60a17329aac Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 9 Nov 2021 02:41:32 -0500 Subject: [PATCH 22/68] Add axis hover rectangles to add a single drop-shadow --- .../core/src/components/graphs/heatmap.ts | 109 ++++++++++-------- packages/core/src/styles/graphs/_heatmap.scss | 19 ++- 2 files changed, 80 insertions(+), 48 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 28cade00f2..ab6c87a7dd 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -92,6 +92,30 @@ export class Heatmap extends Component { }) .attr('aria-label', (d) => d.value); + rectangles.exit().remove(); + + const rowAndColumnHighligher = svg.append('g'); + + // Row + rowAndColumnHighligher + .append('rect') + .classed('highlighter-hidden', true) + .classed('highlighter-row', true) + .attr('x', xRange[0]) + .attr('y', 0) + .attr('width', Math.abs(xRange[1] - xRange[0])) + .attr('height', this.yBandwidth); + + // Column + rowAndColumnHighligher + .append('rect') + .classed('highlighter-hidden', true) + .classed('highlighter-column', true) + .attr('x', xRange[0]) + .attr('y', 0) + .attr('width', this.xBandwidth) + .attr('height', Math.abs(yRange[1] - yRange[0])); + // Add dividers if status is not off, will assume auto or on by default. const dividerStatus = Tools.getProperty( options, @@ -162,9 +186,24 @@ export class Heatmap extends Component { } ); + console.log( + hoveredElement.style('stroke-width') === '1px' + ? '2px' + : '1px', + hoveredElement.style('stroke-width') + ); // Perform visual changes only if value is valid // Highlight element - hoveredElement.raise().classed('raised', true); + hoveredElement + .style( + 'stroke-width', + hoveredElement.style('stroke-width') === '1px' + ? '2px' + : '1px' + ) + .raise() + .classed('raised', true); + // Dispatch tooltip show event self.services.events.dispatchEvent(Events.Tooltip.SHOW, { event, @@ -215,7 +254,7 @@ export class Heatmap extends Component { .on('mouseout', function (event, datum) { const hoveredElement = select(this); const nullState = hoveredElement.classed('null-state'); - hoveredElement.classed('raised', false); + hoveredElement.lower().classed('raised', false); // Add cell divider based on status if (dividerStatus !== DividerStatus.OFF && !nullState) { @@ -269,12 +308,14 @@ export class Heatmap extends Component { // Labels const domainLabel = this.services.cartesianScales.getDomainLabel(); const rangeLabel = this.services.cartesianScales.getRangeLabel(); + // Scales + const mainXScale = this.services.cartesianScales.getMainXScale(); + const mainYScale = this.services.cartesianScales.getMainYScale(); let label = '', sum = 0, min = 0, max = 0; - const ids = []; // Check to see where datum belongs if (this.matrix[datum] != undefined) { @@ -285,10 +326,6 @@ export class Heatmap extends Component { sum += value; min = value < min ? value : min; max = value > max ? value : max; - const id = this.matrix[datum][element].index; - if (id >= 0) { - ids.push(`.heat-${id}`); - } }); } else { label = rangeLabel; @@ -297,20 +334,22 @@ export class Heatmap extends Component { sum += value; min = value < min ? value : min; max = value > max ? value : max; - const id = this.matrix[element][datum].index; - - if (id >= 0) { - ids.push(`rect.heat-${id}`); - } }); } - this.parent - .selectAll(ids.join(',')) - .classed('axis-hovered', true) - .style('stroke', 'white') - .style('stroke-width', '2px') - .raise(); + if (mainXScale(datum)) { + this.parent + .selectAll('rect.highlighter-column') + .classed('highlighter-hidden', false) + .attr('x', mainXScale(datum)) + .raise(); + } else if (mainYScale(datum)) { + this.parent + .selectAll('rect.highlighter-row') + .classed('highlighter-hidden', false) + .attr('y', mainYScale(datum)) + .raise(); + } // Dispatch tooltip show event this.services.events.dispatchEvent(Events.Tooltip.SHOW, { @@ -340,35 +379,11 @@ export class Heatmap extends Component { // Un-highlight all elements handleAxisMouseOut = (event: CustomEvent) => { - const dividerStatus = Tools.getProperty( - this.getOptions(), - 'heatmap', - 'divider', - 'state' - ); - - const hoveredRects = this.parent - .selectAll('rect.axis-hovered') - .classed('axis-hovered', false); - - if ( - (dividerStatus === DividerStatus.AUTO && - Configuration.heatmap.minCellDividerDimension <= - this.xBandwidth && - Configuration.heatmap.minCellDividerDimension <= - this.yBandwidth) || - dividerStatus === DividerStatus.ON - ) { - /** - * @question - * Use Gray 10 on white theme, but what about the others? - */ - hoveredRects - .style('stroke', '#f4f4f4') - .style('stroke-width', '1px'); - } else { - hoveredRects.style('stroke', 'none').style('stroke-width', '0px'); - } + // Hide row/column highlighting + this.parent + .selectAll('rect.highlighter-column,rect.highlighter-row') + .classed('highlighter-hidden', true) + .lower(); // Dispatch hide tooltip event this.services.events.dispatchEvent(Events.Tooltip.HIDE, { diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 01adff3f0f..0fb163216e 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -1,6 +1,23 @@ .#{$prefix}--#{$charts-prefix}--heatmap { rect.raised { - filter: drop-shadow(0px 0px 8px black); + /** + * @todo + * Make stroke color part of theme based on recordings + */ + stroke: #ffffff; + filter: drop-shadow(0px 0px 3px black); + } + + rect.highlighter-hidden { + visibility: hidden; + } + + rect.highlighter-column, + rect.highlighter-row { + stroke: #ffffff; + fill: transparent; + stroke-width: 2px; + filter: drop-shadow(0px 0px 5px black); } @if $carbon--theme == $carbon--theme--white { From 02addcf7cf5f02cd52850f09618e3a626852c399 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 9 Nov 2021 02:44:18 -0500 Subject: [PATCH 23/68] Update demos --- packages/core/demo/data/heatmap.ts | 38 ++++++++++++++++++++++++++---- packages/core/demo/data/index.ts | 7 +++++- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index 53800b65e1..5cd19e1431 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -617,13 +617,27 @@ export const heatmapOptions = { }, }; -export const heatmapColorOptions = { - title: 'Heatmap (Color options)', - heatmap: { - colorPalette: { - type: 'teal', +export const heatmapLegendOptions = { + title: 'Heatmap (Quantize legend options)', + axes: { + bottom: { + title: 'Letters', + mapsTo: 'letter', + scaleType: 'labels', + }, + left: { + title: 'Months', + mapsTo: 'month', + scaleType: 'labels', }, }, + legend: { + colorLegend: { type: 'quantize' }, + }, +}; + +export const heatmapDomainOptions = { + title: 'Heatmap (Axis order option)', axes: { bottom: { title: 'Letters', @@ -634,6 +648,20 @@ export const heatmapColorOptions = { title: 'Months', mapsTo: 'month', scaleType: 'labels', + domain: [ + 'January', + 'February', + 'March', + 'April', + 'May', + 'June', + 'July', + 'August', + 'September', + 'October', + 'November', + 'December', + ], }, }, }; diff --git a/packages/core/demo/data/index.ts b/packages/core/demo/data/index.ts index 631e94cbca..227dae61b2 100644 --- a/packages/core/demo/data/index.ts +++ b/packages/core/demo/data/index.ts @@ -815,7 +815,7 @@ const simpleChartDemos = [ chartType: chartTypes.HeatmapChart, }, { - options: heatmapDemos.heatmapColorOptions, + options: heatmapDemos.heatmapLegendOptions, data: heatmapDemos.heatmapData, chartType: chartTypes.HeatmapChart, }, @@ -824,6 +824,11 @@ const simpleChartDemos = [ data: heatmapDemos.heatmapMissingData, chartType: chartTypes.HeatmapChart, }, + { + options: heatmapDemos.heatmapDomainOptions, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, ], }, { From 1da437a1f6f412f317bf3da7aa44df3d8be5be8e Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 09:19:49 -0500 Subject: [PATCH 24/68] revert --- packages/core/src/axis-chart.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/packages/core/src/axis-chart.ts b/packages/core/src/axis-chart.ts index e0a53bbcd6..05df40f358 100644 --- a/packages/core/src/axis-chart.ts +++ b/packages/core/src/axis-chart.ts @@ -17,7 +17,6 @@ import { ChartClip, Modal, LayoutComponent, - ColorScaleLegend, Legend, Threshold, Highlight, @@ -48,15 +47,6 @@ export class AxisChart extends Chart { configs?: any ) { const options = this.model.getOptions(); - - /** - * @question - * Probably not the best place to put this as it'll be called in other charts - * Probbaly a good idea to create a similar class (almost exact copy - extending won't work) - * with heatmap specific options and layout? - */ - const isHeatmapEnabled = Tools.getProperty(options, 'heatmap'); - const isZoomBarEnabled = Tools.getProperty( options, 'zoomBar', @@ -122,16 +112,10 @@ export class AxisChart extends Chart { const legendComponent = { id: 'legend', - components: [ - isHeatmapEnabled - ? new ColorScaleLegend(this.model, this.services) - : new Legend(this.model, this.services), - ], + components: [new Legend(this.model, this.services)], growth: LayoutGrowth.PREFERRED, }; - // Check to see if it is heatmap here and call the color scale legend - // if all zoom bars are locked, no need to add chart brush if (zoomBarEnabled && !isZoomBarLocked) { graphFrameComponents.push( From 780ed75318219432bd72992b13049b0d7c8bae06 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 09:20:20 -0500 Subject: [PATCH 25/68] Add custom components list in model --- packages/core/src/charts/heatmap.ts | 147 ++++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 6 deletions(-) diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index eb8475046a..043dc4d135 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -2,18 +2,30 @@ import { HeatmapModel } from '../model/heatmap'; import { AxisChart } from '../axis-chart'; import * as Configuration from '../configuration'; -import { ChartConfig, HeatmapChartOptions } from '../interfaces/index'; import { Tools } from '../tools'; -// Components +import { + HeatmapChartOptions, + LayoutDirection, + LayoutGrowth, + ChartConfig, + RenderTypes, + LayoutAlignItems, +} from '../interfaces/index'; + import { Heatmap, TwoDimensionalAxes, + Modal, + LayoutComponent, + ColorScaleLegend, + Title, + AxisChartsTooltip, + Spacer, + Toolbar, // the imports below are needed because of typescript bug (error TS4029) Tooltip, - ColorScaleLegend, - LayoutComponent, -} from '../components/index'; +} from '../components'; export class HeatmapChart extends AxisChart { model = new HeatmapModel(this.services); @@ -37,6 +49,129 @@ export class HeatmapChart extends AxisChart { this.init(holder, chartConfigs); } + // Custom getChartComponents - Implements getChartComponents + // Removes zoombar support and additional `features` that are not supported y heatmap + private getChartComponentsList(graphFrameComponents: any[], configs?: any) { + const options = this.model.getOptions(); + const toolbarEnabled = Tools.getProperty(options, 'toolbar', 'enabled'); + + this.services.cartesianScales.determineAxisDuality(); + this.services.cartesianScales.findDomainAndRangeAxes(); // need to do this before getMainXAxisPosition() + this.services.cartesianScales.determineOrientation(); + + const titleAvailable = !!this.model.getOptions().title; + const titleComponent = { + id: 'title', + components: [new Title(this.model, this.services)], + growth: LayoutGrowth.STRETCH, + }; + + const toolbarComponent = { + id: 'toolbar', + components: [new Toolbar(this.model, this.services)], + growth: LayoutGrowth.PREFERRED, + }; + + const headerComponent = { + id: 'header', + components: [ + new LayoutComponent( + this.model, + this.services, + [ + // always add title to keep layout correct + titleComponent, + ...(toolbarEnabled ? [toolbarComponent] : []), + ], + { + direction: LayoutDirection.ROW, + alignItems: LayoutAlignItems.CENTER, + } + ), + ], + growth: LayoutGrowth.PREFERRED, + }; + + const legendComponent = { + id: 'legend', + components: [new ColorScaleLegend(this.model, this.services)], + growth: LayoutGrowth.PREFERRED, + }; + + const graphFrameComponent = { + id: 'graph-frame', + components: graphFrameComponents, + growth: LayoutGrowth.STRETCH, + renderType: RenderTypes.SVG, + }; + + const isLegendEnabled = + Tools.getProperty(configs, 'legend', 'enabled') !== false && + this.model.getOptions().legend.enabled !== false; + + // Decide the position of the legend in reference to the chart + const fullFrameComponentDirection = LayoutDirection.COLUMN_REVERSE; + + const legendSpacerComponent = { + id: 'spacer', + components: [new Spacer(this.model, this.services, { size: 8 })], + growth: LayoutGrowth.PREFERRED, + }; + + const fullFrameComponent = { + id: 'full-frame', + components: [ + new LayoutComponent( + this.model, + this.services, + [ + ...(isLegendEnabled ? [legendComponent] : []), + ...(isLegendEnabled ? [legendSpacerComponent] : []), + graphFrameComponent, + ], + { + direction: fullFrameComponentDirection, + } + ), + ], + growth: LayoutGrowth.STRETCH, + }; + + const topLevelLayoutComponents = []; + // header component is required for either title or toolbar + if (titleAvailable || toolbarEnabled) { + topLevelLayoutComponents.push(headerComponent); + + const titleSpacerComponent = { + id: 'spacer', + components: [ + new Spacer( + this.model, + this.services, + toolbarEnabled ? { size: 15 } : undefined + ), + ], + growth: LayoutGrowth.PREFERRED, + }; + + topLevelLayoutComponents.push(titleSpacerComponent); + } + topLevelLayoutComponents.push(fullFrameComponent); + + return [ + new AxisChartsTooltip(this.model, this.services), + new Modal(this.model, this.services), + new LayoutComponent( + this.model, + this.services, + topLevelLayoutComponents, + { + direction: LayoutDirection.COLUMN, + } + ), + ]; + } + getComponents() { // Specify what to render inside the graph-frame const graphFrameComponents = [ @@ -44,7 +179,7 @@ export class HeatmapChart extends AxisChart { new Heatmap(this.model, this.services), ]; - const components: any[] = this.getAxisChartComponents( + const components: any[] = this.getChartComponentsList( graphFrameComponents ); return components; From 53093da1638e5d212fc77adc9e513cc31ba95d3d Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 09:20:45 -0500 Subject: [PATCH 26/68] Remove console message --- packages/core/src/components/graphs/heatmap.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index ab6c87a7dd..6e5e718ea8 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -186,12 +186,6 @@ export class Heatmap extends Component { } ); - console.log( - hoveredElement.style('stroke-width') === '1px' - ? '2px' - : '1px', - hoveredElement.style('stroke-width') - ); // Perform visual changes only if value is valid // Highlight element hoveredElement From 285e59eec44adf38e12ef545f582e61cda4e9b27 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 09:22:05 -0500 Subject: [PATCH 27/68] Add custom color legend config to legend options --- packages/core/src/interfaces/components.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/interfaces/components.ts b/packages/core/src/interfaces/components.ts index 23f069e053..820b37ed78 100644 --- a/packages/core/src/interfaces/components.ts +++ b/packages/core/src/interfaces/components.ts @@ -4,7 +4,7 @@ import { Alignments, ToolbarControlTypes, ZoomBarTypes, - ColorLegendType + ColorLegendType, } from './enums'; import { Component } from '../components/component'; import { TruncationOptions } from './truncation'; @@ -51,7 +51,7 @@ export interface LegendOptions { */ colorLegend?: { type: ColorLegendType; - } + }; } /** From 4e5e2f2ee27860644ca9e17b7d3b4783548cb13b Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 10:51:21 -0500 Subject: [PATCH 28/68] Set a fixed legend height --- packages/core/src/styles/components/_color-legend.scss | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 packages/core/src/styles/components/_color-legend.scss diff --git a/packages/core/src/styles/components/_color-legend.scss b/packages/core/src/styles/components/_color-legend.scss new file mode 100644 index 0000000000..e273b4a9a5 --- /dev/null +++ b/packages/core/src/styles/components/_color-legend.scss @@ -0,0 +1,5 @@ +svg.#{$prefix}--#{$charts-prefix}--color-legend { + display: flex; + user-select: none; + height: 28px; +} From 41ef3ee7262771cda09cfb884efe503331fff495 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 10 Nov 2021 11:04:22 -0500 Subject: [PATCH 29/68] Export color legend styles --- packages/core/src/styles/components/index.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/styles/components/index.scss b/packages/core/src/styles/components/index.scss index 404ccc6cc3..dd6e53ac7e 100644 --- a/packages/core/src/styles/components/index.scss +++ b/packages/core/src/styles/components/index.scss @@ -17,3 +17,4 @@ @import './zoom-bar'; @import './highlights'; @import './diagrams/index.scss'; +@import './color-legend'; From 314b7152bcdeb9531b0445f78883e9b892e06278 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Thu, 11 Nov 2021 02:34:07 -0500 Subject: [PATCH 30/68] Adjust stroke width on different divider states --- .../core/src/components/graphs/heatmap.ts | 51 +++++++------------ packages/core/src/styles/graphs/_heatmap.scss | 30 +++++++++-- 2 files changed, 43 insertions(+), 38 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 6e5e718ea8..3a341b5645 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -116,15 +116,23 @@ export class Heatmap extends Component { .attr('width', this.xBandwidth) .attr('height', Math.abs(yRange[1] - yRange[0])); + if (this.determineDividerStatus()) { + rectangles.style('stroke-width', '1px'); + } + + this.addEventListener(); + } + + determineDividerStatus(): boolean { // Add dividers if status is not off, will assume auto or on by default. const dividerStatus = Tools.getProperty( - options, + this.getOptions(), 'heatmap', 'divider', 'state' ); - // Add cell divider based on status + // Determine if cell divider should be displayed if (dividerStatus !== DividerStatus.OFF) { if ( (dividerStatus === DividerStatus.AUTO && @@ -134,17 +142,11 @@ export class Heatmap extends Component { this.yBandwidth) || dividerStatus === DividerStatus.ON ) { - /** - * @question - * Use Gray 10 on white theme, but what about the others? - */ - rectangles - .style('stroke', '#f4f4f4') - .style('stroke-width', '1px'); + return true; } } - this.addEventListener(); + return false; } addEventListener() { @@ -186,7 +188,6 @@ export class Heatmap extends Component { } ); - // Perform visual changes only if value is valid // Highlight element hoveredElement .style( @@ -250,28 +251,12 @@ export class Heatmap extends Component { const nullState = hoveredElement.classed('null-state'); hoveredElement.lower().classed('raised', false); - // Add cell divider based on status - if (dividerStatus !== DividerStatus.OFF && !nullState) { - if ( - (dividerStatus === DividerStatus.AUTO && - Configuration.heatmap.minCellDividerDimension <= - self.xBandwidth && - Configuration.heatmap.minCellDividerDimension <= - self.yBandwidth) || - dividerStatus === DividerStatus.ON - ) { - /** - * @question - * Use Gray 10 on white theme, but what about the others? - */ - hoveredElement - .style('stroke', '#f4f4f4') - .style('stroke-width', '1px'); - } else { - hoveredElement - .style('stroke', 'none') - .style('stroke-width', '0px'); - } + if (self.determineDividerStatus() && !nullState) { + hoveredElement.style('stroke-width', '1px'); + } else { + hoveredElement + .style('stroke', 'none') + .style('stroke-width', '0px'); } // Dispatch mouse out event diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 0fb163216e..9ad85ed07a 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -1,10 +1,6 @@ .#{$prefix}--#{$charts-prefix}--heatmap { rect.raised { - /** - * @todo - * Make stroke color part of theme based on recordings - */ - stroke: #ffffff; + stroke: #ffffff !important; filter: drop-shadow(0px 0px 3px black); } @@ -20,25 +16,49 @@ filter: drop-shadow(0px 0px 5px black); } + rect.null-state { + stroke: transparent !important; + } + + rect.heat { + stroke-width: 0px; + } + @if $carbon--theme == $carbon--theme--white { + rect.heat { + stroke: #ffffff; + } + rect.null-state { fill: #f4f4f4; } } @if $carbon--theme == $carbon--theme--g10 { + rect.heat { + stroke: #f4f4f4; + } + rect.null-state { fill: #ffffff; } } @if $carbon--theme == $carbon--theme--g90 { + rect.heat { + stroke: #262626; + } + rect.null-state { fill: #161616; } } @if $carbon--theme == $carbon--theme--g100 { + rect.heat { + stroke: #161616; + } + rect.null-state { fill: #262626; } From 99a43bf4278ec50ffd61681b645c6da58127679d Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Sat, 13 Nov 2021 21:54:08 -0500 Subject: [PATCH 31/68] Add gradient color class sequence --- packages/core/src/styles/color-palatte.scss | 96 +++++++++++++++++++++ packages/core/src/styles/colors.scss | 32 ++++++- 2 files changed, 127 insertions(+), 1 deletion(-) diff --git a/packages/core/src/styles/color-palatte.scss b/packages/core/src/styles/color-palatte.scss index 50a9cddab5..6096b7e330 100644 --- a/packages/core/src/styles/color-palatte.scss +++ b/packages/core/src/styles/color-palatte.scss @@ -261,3 +261,99 @@ $white-theme-legend-area-item-colors: ( ); $white-theme-legend-area-item-stroke: getColorValue('gray', 50); + +$monochrome-quantize-colors: ( + 'mono-1': ( + '1': #ffffff, + '2': getColorValue('purple', 10), + '3': getColorValue('purple', 20), + '4': getColorValue('purple', 30), + '5': getColorValue('purple', 40), + '6': getColorValue('purple', 50), + '7': getColorValue('purple', 60), + '8': getColorValue('purple', 70), + '9': getColorValue('purple', 80), + '10': getColorValue('purple', 90), + '11': getColorValue('purple', 100), + ), + 'mono-2': ( + '1': #ffffff, + '2': getColorValue('blue', 10), + '3': getColorValue('blue', 20), + '4': getColorValue('blue', 30), + '5': getColorValue('blue', 40), + '6': getColorValue('blue', 50), + '7': getColorValue('blue', 60), + '8': getColorValue('blue', 70), + '9': getColorValue('blue', 80), + '10': getColorValue('blue', 90), + '11': getColorValue('blue', 100), + ), + 'mono-3': ( + '1': #ffffff, + '2': getColorValue('cyan', 10), + '3': getColorValue('cyan', 20), + '4': getColorValue('cyan', 30), + '5': getColorValue('cyan', 40), + '6': getColorValue('cyan', 50), + '7': getColorValue('cyan', 60), + '8': getColorValue('cyan', 70), + '9': getColorValue('cyan', 80), + '10': getColorValue('cyan', 90), + '11': getColorValue('cyan', 100), + ), + 'mono-4': ( + '1': #ffffff, + '2': getColorValue('teal', 10), + '3': getColorValue('teal', 20), + '4': getColorValue('teal', 30), + '5': getColorValue('teal', 40), + '6': getColorValue('teal', 50), + '7': getColorValue('teal', 60), + '8': getColorValue('teal', 70), + '9': getColorValue('teal', 80), + '10': getColorValue('teal', 90), + '11': getColorValue('teal', 100), + ), +); + +$divergent-quantize-colors: ( + 'diverge-1': ( + '1': getColorValue('red', 80), + '2': getColorValue('red', 70), + '3': getColorValue('red', 60), + '4': getColorValue('red', 50), + '5': getColorValue('red', 40), + '6': getColorValue('red', 30), + '7': getColorValue('red', 20), + '8': getColorValue('red', 10), + '9': #ffffff, + '10': getColorValue('cyan', 10), + '11': getColorValue('cyan', 20), + '12': getColorValue('cyan', 30), + '13': getColorValue('cyan', 40), + '14': getColorValue('cyan', 50), + '15': getColorValue('cyan', 60), + '16': getColorValue('cyan', 70), + '17': getColorValue('cyan', 80), + ), + 'diverge-2': ( + '1': getColorValue('purple', 80), + '2': getColorValue('purple', 70), + '3': getColorValue('purple', 60), + '4': getColorValue('purple', 50), + '5': getColorValue('purple', 40), + '6': getColorValue('purple', 30), + '7': getColorValue('purple', 20), + '8': getColorValue('purple', 10), + '9': #ffffff, + '10': getColorValue('teal', 10), + '11': getColorValue('teal', 20), + '12': getColorValue('teal', 30), + '13': getColorValue('teal', 40), + '14': getColorValue('teal', 50), + '15': getColorValue('teal', 60), + '16': getColorValue('teal', 70), + '17': getColorValue('teal', 80), + ), +); diff --git a/packages/core/src/styles/colors.scss b/packages/core/src/styles/colors.scss index 2b094932ef..3d96a5d6eb 100644 --- a/packages/core/src/styles/colors.scss +++ b/packages/core/src/styles/colors.scss @@ -12,6 +12,13 @@ } } +@function getGradientColors() { + $monochrome: color-property(null, $monochrome-quantize-colors); + $divergent: color-property(null, $divergent-quantize-colors); + + @return map-merge($monochrome, $divergent); +} + @function getLegendAreaItemColors() { @if $carbon--theme == $carbon--theme--g100 or @@ -47,8 +54,31 @@ } } +@function gradient-color-property($name, $theme-colors) { + $color-items: (); + + @if type-of($theme-colors) == map { + @each $category, $value in $theme-colors { + @if $name == null { + $color-items: map-merge( + $color-items, + color-property('#{$category}', $value) + ); + } @else { + $color-items: map-merge( + $color-items, + color-property('#{$name}-#{$category}', $value) + ); + } + } + @return $color-items; + } @else { + @return (#{$name}: $theme-colors); + } +} + .#{$prefix}--#{$charts-prefix}--chart-wrapper { - $color-map: getThemeColors(); + $color-map: map-merge(getThemeColors(), getGradientColors()); @each $token, $color in $color-map { .fill-#{$token} { From 9249a17e4fbdb404a1b0f47a889d67018b80a025 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Sat, 13 Nov 2021 22:55:31 -0500 Subject: [PATCH 32/68] Update chart configs & interfaces --- packages/core/src/configuration.ts | 3 --- packages/core/src/interfaces/charts.ts | 33 +++++++------------------- packages/core/src/interfaces/enums.ts | 2 +- 3 files changed, 9 insertions(+), 29 deletions(-) diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index 8275eb7fb0..1c80f4554a 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -598,9 +598,6 @@ const heatmapChart: HeatmapChartOptions = Tools.merge({}, chart, { divider: { state: DividerStatus.AUTO, }, - colorPalette: { - type: 'purple', - }, }, legend: { colorLegend: { diff --git a/packages/core/src/interfaces/charts.ts b/packages/core/src/interfaces/charts.ts index 429e3fdd34..03997e6937 100644 --- a/packages/core/src/interfaces/charts.ts +++ b/packages/core/src/interfaces/charts.ts @@ -138,7 +138,14 @@ export interface BaseChartOptions { * options related to gradient * e.g. { enabled: true } */ - gradient?: object; + gradient?: { + enabled?: boolean; + /** + * hex color array + * e.g. ['#fff', '#000', ...] + */ + colors?: Array; + }; }; } @@ -524,29 +531,5 @@ export interface HeatmapChartOptions extends BaseChartOptions { divider?: { state?: DividerStatus; }; - /** - * @question - Should this be a new config? I'm not sure if color legend will be reused - * Color palette too use - */ - colorPalette?: { - /** - * @question - REQUIRES IMPLEMENTATION REVIEW - * - SHOULD THIS BE PART OF THE COLOR OBJECT INSTEAD? - * Sets which IBM color scheme to use, defaults to 'purple' - */ - type?: - | 'purple' - | 'blue' - | 'cyan' - | 'teal' - | 'red-cyan' - | 'purple-teal'; - /** - * @question - REQUIRES IMPLEMENTATION REVIEW - * - SHOULD THIS BE PART OF THE LEGEND OR COLOR OBJECT INSTEAD? - * Uses the listed colors to generate color scheme (For both heatmap & color-legend); - */ - colorCodes?: Array; - }; }; } diff --git a/packages/core/src/interfaces/enums.ts b/packages/core/src/interfaces/enums.ts index 36e235461d..de83ea322b 100644 --- a/packages/core/src/interfaces/enums.ts +++ b/packages/core/src/interfaces/enums.ts @@ -256,7 +256,7 @@ export enum LegendItemType { */ export enum ColorLegendType { LINEAR = 'linear', - QUANTILE = 'quantile', + QUANTIZE = 'quantize', } /** From 75c8c2d29ae5f3ec5da8d69762c053e17ee7fd1e Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 15 Nov 2021 01:15:34 -0500 Subject: [PATCH 33/68] Add color legend specific non-customizable configs --- packages/core/src/configuration-non-customizable.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/packages/core/src/configuration-non-customizable.ts b/packages/core/src/configuration-non-customizable.ts index c29c9c980b..60181e02ce 100644 --- a/packages/core/src/configuration-non-customizable.ts +++ b/packages/core/src/configuration-non-customizable.ts @@ -130,6 +130,11 @@ export const legend = { iconData: [{ x: 0, y: 0, width: 12, height: 12 }], color: '#8D8D8D', }, + color: { + barWidth: 300, + barHeight: 8, + axisYTranslation: 10, + }, }; export const lines = { From 150e8cb8d9b3606af5390d2b991e7b90640c7178 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 15 Nov 2021 01:18:54 -0500 Subject: [PATCH 34/68] Model changes to support quantize scale --- packages/core/src/model/heatmap.ts | 272 +++++++++++++++-------------- 1 file changed, 145 insertions(+), 127 deletions(-) diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 37ebea6387..7385a0bb2a 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -1,11 +1,8 @@ // Internal Imports -import { ScaleTypes } from '../interfaces'; +import { ColorLegendType, ScaleTypes } from '../interfaces'; import { ChartModelCartesian } from './cartesian-charts'; import { Tools } from '../tools'; -// date formatting -import { format } from 'date-fns'; - // d3 imports import { extent } from 'd3-array'; import { scaleLinear, scaleQuantize } from 'd3-scale'; @@ -14,8 +11,8 @@ import { scaleLinear, scaleQuantize } from 'd3-scale'; export class HeatmapModel extends ChartModelCartesian { private palettes = { // Monochromatic palettes - // Purple 10 - 100 (Includes white) - purple: [ + // White, Purple 10 - 100 + 'mono-1': [ '#ffffff', '#f6f2ff', '#e8daff', @@ -28,8 +25,8 @@ export class HeatmapModel extends ChartModelCartesian { '#31135e', '#1c0f30', ], - // Blue 10 - 100 (Includes white) - blue: [ + // White, Blue 10 - 100 + 'mono-2': [ '#ffffff', '#edf5ff', '#d0e2ff', @@ -42,8 +39,8 @@ export class HeatmapModel extends ChartModelCartesian { '#001d6c', '#001141', ], - // Cyan 10 - 100 (Includes white) - cyan: [ + // White, Cyan 10 - 100 + 'mono-3': [ '#ffffff', '#e5f6ff', '#bae6ff', @@ -56,8 +53,8 @@ export class HeatmapModel extends ChartModelCartesian { '#012749', '#1c0f30', ], - // Teal 10 - 100 (includes white) - teal: [ + // White, Teal 10 - 100 + 'mono-4': [ '#ffffff', '#d9fbfb', '#9ef0f0', @@ -71,8 +68,8 @@ export class HeatmapModel extends ChartModelCartesian { '#081a1c', ], // Diverging palettes - // Red 80 - 10, Cyan 10 - 80 - 'red-cyan': [ + // Red 80 - 10, White, Cyan 10 - 80 + 'diverge-1': [ '#750e13', '#a2191f', '#da1e28', @@ -81,6 +78,7 @@ export class HeatmapModel extends ChartModelCartesian { '#ffb3b8', '#ffd7d9', '#fff1f1', + '#ffffff', '#e5f6ff', '#bae6ff', '#82cfff', @@ -91,7 +89,7 @@ export class HeatmapModel extends ChartModelCartesian { '#003a6d', ], // Purple 80 - 10, Teal 10 - 80 - 'purple-teal': [ + 'diverge-2': [ '#491d8b', '#6929c4', '#8a3ffc', @@ -100,6 +98,8 @@ export class HeatmapModel extends ChartModelCartesian { '#d4bbff', '#e8daff', '#f6f2ff', + '#fff1f1', + '#ffffff', '#d9fbfb', '#9ef0f0', '#3ddbd9', @@ -111,13 +111,13 @@ export class HeatmapModel extends ChartModelCartesian { ], }; - // Will hold linearScale used in tick creation & colors - private _linearScale: any = undefined; + private colorScaleType: ColorLegendType = ColorLegendType.LINEAR; + selectedPalette = []; private _colorScale: any = undefined; // List of unique ranges and domains private _domains = []; - private _range = []; + private _ranges = []; private _matrix = {}; @@ -138,28 +138,14 @@ export class HeatmapModel extends ChartModelCartesian { (!!Tools.getProperty(axis, 'top', 'scaleType') && Tools.getProperty(axis, 'top', 'scaleType') !== ScaleTypes.LABELS) || - !!Tools.getProperty( - axis, - 'bottom', - 'scaleType' && - Tools.getProperty(axis, 'bottom', 'scaleType') !== - ScaleTypes.LABELS - ) + (!!Tools.getProperty(axis, 'bottom', 'scaleType') && + Tools.getProperty(axis, 'bottom', 'scaleType') !== + ScaleTypes.LABELS) ) { throw Error('Heatmap only supports label scaletypes.'); } } - getLinearScale() { - if (!this._linearScale) { - this._linearScale = scaleLinear() - .domain(this.getValueDomain()) - .range([0, 75]); - } - - return this._linearScale; - } - /** * Get min and maximum value of the display data * @returns Array consisting of smallest and largest values in data @@ -169,65 +155,45 @@ export class HeatmapModel extends ChartModelCartesian { const limits = extent(data); const domain = []; - // Round extent values to the nearest 10th values since axis rounds values to multiples of 2, 5, and 10s. - limits.forEach((number) => { + // Round extent values to the nearest multiple of 50 + // Axis rounds values to multiples of 2, 5, and 10s. + limits.forEach((number, index) => { let value = Number(number); - if (value % 10 === 0 || value === 0) { + if (index === 0 && value >= 0) { + value = 0; + } else if (value % 50 === 0 || value === 0) { value; } else if (value < 0) { - value = Math.floor(value / 10) * 10; + value = Math.floor(value / 50) * 50; } else { - value = Math.ceil(value / 10) * 10; + value = Math.ceil(value / 50) * 50; } domain.push(value); }); - const options = this.getOptions(); - if ( - Tools.getProperty(options, 'legend', 'colorLegend', 'type') === - 'linear' - ) { - return domain; + // Ensure the median of the range is 0 + if (domain[0] < 0 && domain[1] > 0) { + if (Math.abs(domain[0]) > domain[1]) { + domain[1] = Math.abs(domain[0]); + } else { + domain[0] = -domain[1]; + } } - /** - * @todo - * Clean this up! - */ - return domain.length !== 2 ? [0, 1] : [0, 100]; + return domain; } /** - * + * @override * @param value * @returns */ getFillColor(value: number) { - if (!this._colorScale) { - this.getColorScale(); - } - return this._colorScale(value); } - /** - * Returns linear color scale - * @returns Scale - */ - getColorScale() { - if (!this._colorScale) { - this._colorScale = scaleQuantize() - .domain(this.getValueDomain() as [number, number]) - // scaleLinear() - // .domain(this.getTicks()) - .range(this.getPalettes()); - } - - return this._colorScale; - } - /** * Generate a list of all unique domains * @returns String[] @@ -266,7 +232,7 @@ export class HeatmapModel extends ChartModelCartesian { * @returns String[] */ getUniqueRanges(): string[] { - if (Tools.isEmpty(this._range)) { + if (Tools.isEmpty(this._ranges)) { const displayData = this.getDisplayData(); const { cartesianScales } = this.services; @@ -282,7 +248,7 @@ export class HeatmapModel extends ChartModelCartesian { } // Get unique axis values & create a matrix - this._range = Array.from( + this._ranges = Array.from( new Set( displayData.map((d) => { return d[rangeIdentifier]; @@ -291,7 +257,7 @@ export class HeatmapModel extends ChartModelCartesian { ); } - return this._range; + return this._ranges; } /** @@ -342,11 +308,6 @@ export class HeatmapModel extends ChartModelCartesian { const uniqueDomain = this.getUniqueDomain(); const uniqueRange = this.getUniqueRanges(); - /** - * @todo - * - Multiply uniqueDomain.length by uniqueRange.length to get total possible values - * - If displayData().length matches array multiple, return displayData to improve performance - */ const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); @@ -367,19 +328,6 @@ export class HeatmapModel extends ChartModelCartesian { return arr; } - /** - * Generate ticks to display based on available colors in list - * @returns Array - */ - getTicks() { - const extent = this.getValueDomain(); - const colors = this.getPalettes().length; - return scaleLinear() - .domain([extent[0], extent[1]]) - .nice() - .ticks(colors); - } - /** * Generate tabular data from display data * @returns Array @@ -387,7 +335,6 @@ export class HeatmapModel extends ChartModelCartesian { getTabularDataArray() { const displayData = this.getDisplayData(); - const { cartesianScales } = this.services; const { primaryDomain, primaryRange, @@ -395,11 +342,7 @@ export class HeatmapModel extends ChartModelCartesian { secondaryRange, } = this.assignRangeAndDomains(); - const domainScaleType = cartesianScales.getDomainAxisScaleType(); let domainValueFormatter; - if (domainScaleType === ScaleTypes.TIME) { - domainValueFormatter = (d) => format(d, 'MMM d, yyyy'); - } const result = [ [ @@ -441,44 +384,119 @@ export class HeatmapModel extends ChartModelCartesian { return result; } - /** - * Returns colors - * @returns Array - */ - getPalettes() { - const type = Tools.getProperty( - this.getOptions(), - 'heatmap', - 'colorPalette', - 'type' + // Uses quantize scale to return class names + getColorClassName(configs: { value?: number; originalClassName?: string }) { + if ( + typeof configs.value === 'number' && + this.colorScaleType !== ColorLegendType.QUANTIZE + ) { + return configs.originalClassName; + } + + return `${configs.originalClassName} ${this._colorScale( + configs.value as number + )}`; + } + + protected setColorClassNames() { + const options = this.getOptions(); + + const customColors = Tools.getProperty( + options, + 'color', + 'gradient', + 'colors' ); + const customColorsEnabled = !Tools.isEmpty(customColors); - const customColorPalette = Tools.getProperty( - this.getOptions(), - 'heatmap', - 'colorPalette', - 'colorCodes' + let colorPairingOption = Tools.getProperty( + options, + 'color', + 'pairing', + 'option' ); - // If user pass in custom colors, use custom colors - if (customColorPalette?.length) { - return customColorPalette; - } + const colorScaleType = Tools.getProperty( + options, + 'legend', + 'colorLegend', + 'type' + ); // If domain consists of negative and positive values, use diverging palettes const domain = this.getValueDomain(); - if (domain[0] < 0 && domain[1] > 0) { - // If type is not set to available options, use default - if (type !== 'red-cyan' && type !== 'purple-teal') { - return this.palettes['red-cyan']; - } - } + const colorScheme = domain[0] < 0 && domain[1] > 0 ? 'diverge' : 'mono'; - // Check if value exists, if it doesn't use default - if (!type || !this.palettes[type]) { - return this.palettes['purple']; + // Use default color pairing options if not in defined range + if ( + colorPairingOption < 1 && + colorPairingOption > 4 && + colorScheme === 'mono' + ) { + colorPairingOption = 1; + } else if ( + colorPairingOption < 1 && + colorPairingOption > 2 && + colorScheme === 'diverge' + ) { + colorPairingOption = 1; } - return this.palettes[type]; + // Define color scale based on legend + if (colorScaleType === ColorLegendType.LINEAR) { + // Uses hardcoded fill on element + this.colorScaleType = ColorLegendType.LINEAR; + + const colorPairing = customColorsEnabled ? customColors : []; + let ticks = []; + + // Use only the first, middle, and last color to determine the color gradient + // since they are the only displayed ticks + if (!customColorsEnabled) { + const palette = this.palettes[ + `${colorScheme}-${colorPairingOption}` + ]; + colorPairing.push(palette[0]); + colorPairing.push(palette[Math.floor(palette.length / 2)]); + colorPairing.push(palette[palette.length - 1]); + ticks = + colorScheme === 'diverge' + ? [domain[0], 0, domain[1]] + : [domain[0], domain[1] / 2, domain[1]]; + } else { + ticks = scaleLinear() + .domain([domain[0], domain[1]]) + .nice() + .ticks(colorPairing.length); + } + + // Save scale type + this.selectedPalette = colorPairing; + this._colorScale = scaleLinear().domain(ticks).range(colorPairing); + } else if (colorScaleType === ColorLegendType.QUANTIZE) { + // Uses css classes for fill + this.colorScaleType = ColorLegendType.QUANTIZE; + + const colorPairing = customColorsEnabled ? customColors : []; + + if (!customColorsEnabled) { + // Add class names to list and the amount based on the color scheme + // Carbon charts has 11 colors for a single monochromatic palette & 17 for a divergent palette + const colorGroupingLength = colorScheme === 'diverge' ? 17 : 11; + for (let i = 1; i < colorGroupingLength + 1; i++) { + colorPairing.push( + `fill-${colorScheme}-${colorPairingOption}-${i}` + ); + } + } + + // Save scale type + this.selectedPalette = colorPairing; + this._colorScale = scaleQuantize() + .domain(this.getValueDomain() as [number, number]) + .range(colorPairing); + } else { + throw Error(`Color legend ${colorScaleType} is not supported`); + } } } From 94e3c93f8ec6dfa5e46ffbef76d034d942889f72 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 15 Nov 2021 01:19:29 -0500 Subject: [PATCH 35/68] Use css classes for fill color if quantize scale is used --- packages/core/src/components/graphs/heatmap.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 3a341b5645..35ced0537c 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -1,6 +1,5 @@ // Internal Imports import { Component } from '../component'; -import { DOMUtils } from '../../services'; import * as Configuration from '../../configuration'; import { Events, RenderTypes, DividerStatus } from '../../interfaces'; import { Tools } from '../../tools'; @@ -75,6 +74,12 @@ export class Heatmap extends Component { .enter() .append('rect') .attr('class', (d) => `heat-${d.index}`) + .attr('class', (d) => { + return this.model.getColorClassName({ + value: d.value, + originalClassName: `heat-${d.index}`, + }); + }) .classed('heat', true) .classed('null-state', (d) => d.index === -1 || d.value === null ? true : false From d6f3e13ff7ae984cf2be7e42895a6714837b2701 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 15 Nov 2021 01:19:54 -0500 Subject: [PATCH 36/68] Implement quantize legend support --- .../essentials/color-scale-legend.ts | 176 ++++++++++++------ 1 file changed, 114 insertions(+), 62 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 034e1f8fae..f1dd30d020 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -1,19 +1,18 @@ // Internal Imports import { Tools } from '../../tools'; import { - Alignments, RenderTypes, Roles, - Events, TruncationTypes, + ColorLegendType, } from '../../interfaces'; import * as Configuration from '../../configuration'; -import { Legend } from './legend'; -import { DOMUtils } from '../../services'; - // D3 imports import { axisBottom } from 'd3-axis'; +import { Legend } from '..'; +import { scaleBand, scaleLinear } from 'd3-scale'; +import { interpolateRound, quantize } from 'd3-interpolate'; export class ColorScaleLegend extends Legend { type = 'color-legend'; @@ -23,67 +22,120 @@ export class ColorScaleLegend extends Legend { 'gradient-id-' + Math.floor(Math.random() * 99999999999); render() { + const options = this.getOptions(); + // svg and container widths - const svg = this.getComponentContainer({ withinChartClip: true }); - svg.html(''); - const { width, height } = DOMUtils.getSVGElementSize(svg, { - useAttrs: true, - }); + const svg = this.getComponentContainer(); + svg.html('').attr('role', Roles.GROUP); - const options = this.getOptions(); - const legendOptions = Tools.getProperty(options, 'legend'); - - const colors = this.model.getPalettes(); - const ticks = this.model.getTicks(); - const linarScale = this.model.getLinearScale().range([0, 250]); - const stopLength = 100 / colors.length; - - /** - * @todo - Add legend orientation support - * Need designer feedback? - */ - const legendOrientation = Tools.getProperty( + const customColors = Tools.getProperty( options, - 'legend', - 'orientation' + 'color', + 'gradient', + 'colors' ); + const customColorsEnabled = !Tools.isEmpty(customColors); + + const palette = this.model.selectedPalette; + const domain = this.model.getValueDomain(); + + const group = svg.append('g'); + + if (this.model.colorScaleType === ColorLegendType.LINEAR) { + const stopLengthPercentage = 100 / (palette.length - 1); + + // Generate the gradient + const linearGradient = group + .append('linearGradient') + .attr('id', `${this.gradient_id}-legend`) + .selectAll('stop') + .data(palette) + .enter() + .append('stop') + .attr('offset', (_, i) => `${i * stopLengthPercentage}%`) + .attr('stop-color', (d) => d); + + const rectangle = group + .append('rect') + .attr('width', Configuration.legend.color.barWidth) + .attr('height', Configuration.legend.color.barHeight) + .style('fill', `url(#${this.gradient_id}-legend)`); + + // Create scale & ticks + const linearScale = scaleLinear() + .domain(domain) + .range([0, Configuration.legend.color.barWidth]); + domain.splice(1, 0, (domain[0] + domain[1]) / 2); + + const xAxis = axisBottom(linearScale) + .tickSize(0) + .tickValues(domain); + + // Align axes at the bottom of the rectangle and delete the domain line + const axis = group + .append('g') + .attr( + 'transform', + `translate(0,${Configuration.legend.color.axisYTranslation})` + ) + .call(xAxis); + + // Remove domain + axis.select('.domain').remove(); + + // Align text to fit in container + axis.style('text-anchor', 'start'); + } else if (this.model.colorScaleType === ColorLegendType.QUANTIZE) { + const colorScaleBand = scaleBand() + .domain(palette) + .rangeRound([0, Configuration.legend.color.barWidth]); + + // Generate equal chunks between range to act as ticks + const interpolator = interpolateRound(domain[0], domain[1]); + const quant = quantize(interpolator, palette.length); + + const rect = group + .selectAll('rect') + .data(colorScaleBand.domain()) + .join('rect') + .attr('x', colorScaleBand) + .attr('y', 0) + .attr('width', Math.max(0, colorScaleBand.bandwidth() - 1)) + .attr('height', Configuration.legend.color.barHeight); + + // Use attribute fill or css depending on custom Colors + if (customColorsEnabled) { + rect.attr('fill', (_, i) => { + return palette[i]; + }); + } else { + rect.attr('class', (_, i) => { + return palette[i]; + }); + } + + const xAxis = axisBottom(colorScaleBand) + .tickSize(0) + .tickValues(palette) + .tickFormat((_, i) => { + // Use the quant interpolators as ticks + return quant[i].toString(); + }); - const group = svg - .append('g') - /** - * @todo - Determine translation value so that initial value isn't trimmed - */ - .attr('transform', `translate(18, 0)`); - - // Generate the gradient - const linearGradient = group - .append('linearGradient') - .attr('id', (d) => `${this.gradient_id}-legend`) - .selectAll('stop') - .data(colors) - .enter() - .append('stop') - .attr('offset', (d, i) => `${i * stopLength}%`) - .attr('stop-color', (d) => d); - - const rectangle = group - .append('rect') - /** - * @todo - determine width & height - * x offset (or padding) to prevent the first letter from being clipped - */ - .attr('width', '250px') - .attr('height', '18px') - .style('fill', `url(#${this.gradient_id}-legend)`); - - const xAxis = axisBottom(linarScale).tickSize(0).tickValues(ticks); - - // Align axes at the bottom of the rectangle and delete the domain line - group - .append('g') - .attr('transform', 'translate(0,18)') - .call(xAxis) - .select('.domain') - .remove(); + // Align axis to match bandwidth start after initial (white) + group + .append('g') + .attr( + 'transform', + `translate(${colorScaleBand.bandwidth() / 2}, ${ + Configuration.legend.color.axisYTranslation + })` + ) + .call(xAxis) + .select('.domain') + .remove(); + } else { + throw Error('Entered color legend type is not supported.'); + } } } From e4652a6a1ee95fb6c3ce188ac159d8efdf2faeee Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 22 Nov 2021 11:55:44 -0500 Subject: [PATCH 37/68] Extend axis and add tab index support to heatmap --- packages/core/src/charts/heatmap.ts | 4 +- .../core/src/components/axes/hover-axis.ts | 202 ++++++++++++++++++ .../axes/two-dimensional-hover-axes.ts | 121 +++++++++++ packages/core/src/components/index.ts | 1 + .../core/src/styles/components/_axis.scss | 41 ++++ 5 files changed, 367 insertions(+), 2 deletions(-) create mode 100644 packages/core/src/components/axes/hover-axis.ts create mode 100644 packages/core/src/components/axes/two-dimensional-hover-axes.ts diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index 043dc4d135..7c06361608 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -15,7 +15,7 @@ import { import { Heatmap, - TwoDimensionalAxes, + TwoDimensionalHoverAxes, Modal, LayoutComponent, ColorScaleLegend, @@ -175,7 +175,7 @@ export class HeatmapChart extends AxisChart { getComponents() { // Specify what to render inside the graph-frame const graphFrameComponents = [ - new TwoDimensionalAxes(this.model, this.services), + new TwoDimensionalHoverAxes(this.model, this.services), new Heatmap(this.model, this.services), ]; diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts new file mode 100644 index 0000000000..15f580ffee --- /dev/null +++ b/packages/core/src/components/axes/hover-axis.ts @@ -0,0 +1,202 @@ +// Internal Imports +import { Axis } from './axis'; +import { + AxisPositions, + Events, + ScaleTypes, + Roles, + TruncationTypes, +} from '../../interfaces'; +import { Tools } from '../../tools'; +import { ChartModel } from '../../model/model'; +import { DOMUtils } from '../../services'; +import { AxisTitleOrientations, TickRotations } from '../../interfaces/enums'; +import * as Configuration from '../../configuration'; + +// D3 Imports +import { select } from 'd3-selection'; +import { axisBottom, axisLeft, axisRight, axisTop } from 'd3-axis'; + +export class HoverAxis extends Axis { + constructor(model: ChartModel, services: any, configs?: any) { + super(model, services, configs); + } + + render(animate = true) { + super.render(animate); + const { position: axisPosition } = this.configs; + const svg = this.getComponentContainer(); + const container = DOMUtils.appendOrSelect( + svg, + `g.axis.${axisPosition}` + ); + + container.selectAll('g.tick').each(function () { + const g = select(this); + g.classed('tick-hover', true).attr('tabindex', 0); + const textNode = g.select('text'); + const { width, height } = DOMUtils.getSVGElementSize(textNode, { + useBBox: true, + }); + + const rectangle = DOMUtils.appendOrSelect(g, `rect.axis-holder`); + + let x = 0, + y = 0; + + switch (axisPosition) { + case AxisPositions.LEFT: + x = -width + Number(textNode.attr('x')); + y = -(height / 2) + 1; + break; + case AxisPositions.RIGHT: + x = Math.abs(Number(textNode.attr('x'))); + y = -(height / 2); + break; + case AxisPositions.TOP: + x = -(width / 2); + y = -height + Number(textNode.attr('y')) / 2; + break; + case AxisPositions.BOTTOM: + x = -(width / 2); + y = height / 2 - 2; + break; + } + + rectangle + .attr('x', x) + .attr('y', y) + .attr('width', width) + .attr('height', height); + + rectangle.lower(); + }); + + // Add event listeners to elements drawn + this.addEventListeners(); + } + + addEventListeners() { + const svg = this.getComponentContainer(); + const { position: axisPosition } = this.configs; + const container = DOMUtils.appendOrSelect( + svg, + `g.axis.${axisPosition}` + ); + const options = this.getOptions(); + const axisOptions = Tools.getProperty(options, 'axes', axisPosition); + const axisScaleType = Tools.getProperty(axisOptions, 'scaleType'); + const truncationThreshold = Tools.getProperty( + axisOptions, + 'truncation', + 'threshold' + ); + + const self = this; + container + .selectAll('g.tick text') + .on('mouseover', function (event, datum) { + // Dispatch mouse event + self.services.events.dispatchEvent( + Events.Axis.LABEL_MOUSEOVER, + { + event, + element: select(this), + datum, + } + ); + + if ( + axisScaleType === ScaleTypes.LABELS && + datum.length > truncationThreshold + ) { + self.services.events.dispatchEvent(Events.Tooltip.SHOW, { + event, + hoveredElement: select(this), + content: datum, + }); + } + }) + .on('mousemove', function (event, datum) { + // Dispatch mouse event + self.services.events.dispatchEvent( + Events.Axis.LABEL_MOUSEMOVE, + { + event, + element: select(this), + datum, + } + ); + if ( + axisScaleType === ScaleTypes.LABELS && + datum.length > truncationThreshold + ) { + console.log('inside'); + self.services.events.dispatchEvent(Events.Tooltip.MOVE, { + event, + }); + } + }) + .on('click', function (event, datum) { + // Dispatch mouse event + self.services.events.dispatchEvent(Events.Axis.LABEL_CLICK, { + event, + element: select(this), + datum, + }); + }) + .on('mouseout', function (event, datum) { + // Dispatch mouse event + self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, { + event, + element: select(this), + datum, + }); + + if (axisScaleType === ScaleTypes.LABELS) { + self.services.events.dispatchEvent(Events.Tooltip.HIDE); + } + }); + + // Emit mouseover & mouseout events on focus/blur + container + .selectAll('g.tick.tick-hover') + .on('focus', function (event) { + // Dispatch mouse event + self.services.events.dispatchEvent( + Events.Axis.LABEL_MOUSEOVER, + { + event, + element: select(this), + datum: select(this).select('text').datum(), + } + ); + }) + .on('blur', function (event) { + // Dispatch mouse event + self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, { + event, + element: select(this), + datum: select(this).select('text').datum(), + }); + }); + } + + destroy() { + const svg = this.getComponentContainer(); + const { position: axisPosition } = this.configs; + const container = DOMUtils.appendOrSelect( + svg, + `g.axis.${axisPosition}` + ); + + // Remove event listeners + container + .selectAll('g.tick text') + .on('mouseover', null) + .on('mousemove', null) + .on('mouseout', null) + .on('focus', null) + .on('blur', null); + } +} diff --git a/packages/core/src/components/axes/two-dimensional-hover-axes.ts b/packages/core/src/components/axes/two-dimensional-hover-axes.ts new file mode 100644 index 0000000000..e458f2c135 --- /dev/null +++ b/packages/core/src/components/axes/two-dimensional-hover-axes.ts @@ -0,0 +1,121 @@ +// Internal Imports +import { TwoDimensionalAxes } from './two-dimensional-axes'; +import { AxisPositions, RenderTypes } from '../../interfaces'; +import { Tools } from '../../tools'; +import { DOMUtils } from '../../services'; +import { Threshold } from '../essentials/threshold'; +import { Events } from './../../interfaces'; +import { HoverAxis } from './hover-axis'; + +export class TwoDimensionalHoverAxes extends TwoDimensionalAxes { + render(animate = false) { + const axes = {}; + const axisPositions = Object.keys(AxisPositions); + const axesOptions = Tools.getProperty(this.getOptions(), 'axes'); + + axisPositions.forEach((axisPosition) => { + const axisOptions = axesOptions[AxisPositions[axisPosition]]; + if (axisOptions) { + axes[AxisPositions[axisPosition]] = true; + } + }); + + this.configs.axes = axes; + + // Check the configs to know which axes need to be rendered + axisPositions.forEach((axisPositionKey) => { + const axisPosition = AxisPositions[axisPositionKey]; + if ( + this.configs.axes[axisPosition] && + !this.children[axisPosition] + ) { + const axisComponent = new HoverAxis(this.model, this.services, { + position: axisPosition, + axes: this.configs.axes, + margins: this.margins, + }); + + // Set model, services & parent for the new axis component + axisComponent.setModel(this.model); + axisComponent.setServices(this.services); + axisComponent.setParent(this.parent); + + this.children[axisPosition] = axisComponent; + } + }); + + Object.keys(this.children).forEach((childKey) => { + const child = this.children[childKey]; + child.render(animate); + }); + + const margins = {} as any; + + Object.keys(this.children).forEach((childKey) => { + const child = this.children[childKey]; + const axisPosition = child.configs.position; + + // Grab the invisibly rendered axis' width & height, and set margins + // Based off of that + // We draw the invisible axis because of the async nature of d3 transitions + // To be able to tell the final width & height of the axis when initiaing the transition + // The invisible axis is updated instantly and without a transition + const invisibleAxisRef = child.getInvisibleAxisRef(); + const { + width, + height, + } = DOMUtils.getSVGElementSize(invisibleAxisRef, { useBBox: true }); + + let offset; + if (child.getTitleRef().empty()) { + offset = 0; + } else { + offset = DOMUtils.getSVGElementSize(child.getTitleRef(), { + useBBox: true, + }).height; + + if ( + axisPosition === AxisPositions.LEFT || + axisPosition === AxisPositions.RIGHT + ) { + offset += 5; + } + } + + switch (axisPosition) { + case AxisPositions.TOP: + margins.top = height + offset; + break; + case AxisPositions.BOTTOM: + margins.bottom = height + offset; + break; + case AxisPositions.LEFT: + margins.left = width + offset; + break; + case AxisPositions.RIGHT: + margins.right = width + offset; + break; + } + }); + + // If the new margins are different than the existing ones + const isNotEqual = Object.keys(margins).some((marginKey) => { + return this.margins[marginKey] !== margins[marginKey]; + }); + + if (isNotEqual) { + this.margins = Object.assign(this.margins, margins); + + // also set new margins to model to allow external components to access + this.model.set({ axesMargins: this.margins }, { skipUpdate: true }); + this.services.events.dispatchEvent(Events.ZoomBar.UPDATE); + + Object.keys(this.children).forEach((childKey) => { + const child = this.children[childKey]; + child.margins = this.margins; + }); + + this.render(true); + } + } +} diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index c7e1b238f3..491b32c208 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -45,6 +45,7 @@ export * from './layout/layout'; // MISC export * from './axes/two-dimensional-axes'; +export * from './axes/two-dimensional-hover-axes'; export * from './axes/axis'; export * from './axes/grid-brush'; export * from './axes/chart-clip'; diff --git a/packages/core/src/styles/components/_axis.scss b/packages/core/src/styles/components/_axis.scss index 85a8e58dd1..cc16dd4ebd 100644 --- a/packages/core/src/styles/components/_axis.scss +++ b/packages/core/src/styles/components/_axis.scss @@ -10,6 +10,47 @@ visibility: hidden; } + g.tick-hover rect.axis-holder { + fill: transparent; + stroke: transparent; + stroke-width: 2px; + } + + g.tick-hover:hover, + g.tick-hover:focus { + @if $carbon--theme == + $carbon--theme--g90 or + $carbon--theme == + $carbon--theme--g100 + { + rect.axis-holder { + fill: white; + stroke: white; + stroke-width: 2px; + } + + text { + fill: invert($text-02); + } + } + + @if $carbon--theme == + $carbon--theme--g10 or + $carbon--theme == + $carbon--theme--white + { + rect.axis-holder { + fill: black; + stroke: black; + stroke-width: 2px; + } + + text { + fill: white; + } + } + } + g.tick text { fill: $text-02; font-family: carbon--font-family('sans-condensed'); From 1b0ed0ebf1f84ee92c638899feb009f12b664198 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 24 Nov 2021 02:48:12 -0500 Subject: [PATCH 38/68] Improve axis highlight event --- .../core/src/components/graphs/heatmap.ts | 131 +++++++++++++++--- packages/core/src/styles/graphs/_heatmap.scss | 13 +- 2 files changed, 119 insertions(+), 25 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 35ced0537c..ac7736ddfd 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -35,10 +35,9 @@ export class Heatmap extends Component { render(animate = true) { // svg and container widths - const svg = this.getComponentContainer({ withinChartClip: true }); + const svg = this.getComponentContainer(); const { cartesianScales } = this.services; - const options = this.model.getOptions(); this.matrix = this.model.getMatrix(); svg.html(''); @@ -68,12 +67,29 @@ export class Heatmap extends Component { (yRange[1] - yRange[0]) / uniqueRange.length ); - const rectangles = svg + const dividerStatus = this.determineDividerStatus(); + const container = svg + .append('g') + .attr( + 'transform', + dividerStatus ? `translate(1, -1)` : `translate(0, 0)` + ); + + const rectangles = container .selectAll() .data(matrixArray) .enter() - .append('rect') + .append('g') .attr('class', (d) => `heat-${d.index}`) + .classed('cell', true) + .attr( + 'transform', + (d) => + `translate(${mainXScale(d[domainIdentifier])}, ${mainYScale( + d[rangeIdentifier] + )})` + ) + .append('rect') .attr('class', (d) => { return this.model.getColorClassName({ value: d.value, @@ -84,8 +100,6 @@ export class Heatmap extends Component { .classed('null-state', (d) => d.index === -1 || d.value === null ? true : false ) - .attr('x', (d) => mainXScale(d[domainIdentifier])) - .attr('y', (d) => mainYScale(d[rangeIdentifier])) .attr('width', this.xBandwidth) .attr('height', this.yBandwidth) .style('fill', (d) => { @@ -99,23 +113,46 @@ export class Heatmap extends Component { rectangles.exit().remove(); - const rowAndColumnHighligher = svg.append('g'); + const rowAndColumnShadow = container + .append('g') + .classed('shadow-holder', true); // Row - rowAndColumnHighligher + rowAndColumnShadow .append('rect') .classed('highlighter-hidden', true) - .classed('highlighter-row', true) + .classed('shadow-row', true) .attr('x', xRange[0]) .attr('y', 0) .attr('width', Math.abs(xRange[1] - xRange[0])) .attr('height', this.yBandwidth); - // Column - rowAndColumnHighligher + rowAndColumnShadow .append('rect') .classed('highlighter-hidden', true) - .classed('highlighter-column', true) + .classed('shadow-column', true) + .attr('x', xRange[0]) + .attr('y', 0) + .attr('width', this.xBandwidth) + .attr('height', Math.abs(yRange[1] - yRange[0])); + + const rowAndColumnHighlighter = container + .append('g') + .classed('row-column-highlighter', true); + + rowAndColumnHighlighter + .append('rect') + .classed('highlighter-hidden', true) + .classed('highlight-row', true) + .attr('x', xRange[0]) + .attr('y', 0) + .attr('width', Math.abs(xRange[1] - xRange[0])) + .attr('height', this.yBandwidth); + + rowAndColumnHighlighter + .append('rect') + .classed('highlighter-hidden', true) + .classed('highlight-column', true) .attr('x', xRange[0]) .attr('y', 0) .attr('width', this.xBandwidth) @@ -174,9 +211,10 @@ export class Heatmap extends Component { const rangeLabel = cartesianScales.getRangeLabel(); this.parent - .selectAll('rect.heat') + .selectAll('g.cell') .on('mouseover', function (event, datum) { - const hoveredElement = select(this); + const cell = select(this); + const hoveredElement = cell.select('rect.heat'); const nullState = hoveredElement.classed('null-state'); // Dispatch event and tooltip only if value exists @@ -193,6 +231,8 @@ export class Heatmap extends Component { } ); + cell.raise(); + // Highlight element hoveredElement .style( @@ -201,7 +241,6 @@ export class Heatmap extends Component { ? '2px' : '1px' ) - .raise() .classed('raised', true); // Dispatch tooltip show event @@ -252,9 +291,11 @@ export class Heatmap extends Component { ); }) .on('mouseout', function (event, datum) { - const hoveredElement = select(this); + const cell = select(this); + const hoveredElement = cell.select('rect.heat'); const nullState = hoveredElement.classed('null-state'); - hoveredElement.lower().classed('raised', false); + hoveredElement.classed('raised', false); + cell.lower(); if (self.determineDividerStatus() && !nullState) { hoveredElement.style('stroke-width', '1px'); @@ -300,6 +341,7 @@ export class Heatmap extends Component { sum = 0, min = 0, max = 0; + const ids = []; // Check to see where datum belongs if (this.matrix[datum] != undefined) { @@ -310,6 +352,10 @@ export class Heatmap extends Component { sum += value; min = value < min ? value : min; max = value > max ? value : max; + const id = this.matrix[datum][element].index; + if (id >= 0) { + ids.push(`g.heat-${id}`); + } }); } else { label = rangeLabel; @@ -318,21 +364,48 @@ export class Heatmap extends Component { sum += value; min = value < min ? value : min; max = value > max ? value : max; + const id = this.matrix[datum][element].index; + if (id >= 0) { + ids.push(`g.heat-${id}`); + } }); } + // Pop out cells + this.parent + .selectAll(ids.join(',')) + .classed('axis-hovered', true) + .raise(); + + // Show the outlines to make the cells appear grouped + const strokeHighlighter = this.parent + .selectAll('g.row-column-highlighter') + .raise(); + if (mainXScale(datum)) { this.parent - .selectAll('rect.highlighter-column') + .select('rect.shadow-column') + .classed('highlighter-hidden', false) + .attr('x', mainXScale(datum)) + .raise(); + + strokeHighlighter + .select('rect.highlight-column') .classed('highlighter-hidden', false) .attr('x', mainXScale(datum)) .raise(); } else if (mainYScale(datum)) { this.parent - .selectAll('rect.highlighter-row') + .select('rect.shadow-row') .classed('highlighter-hidden', false) .attr('y', mainYScale(datum)) .raise(); + + strokeHighlighter + .select('rect.highlight-row') + .classed('highlighter-hidden', false) + .attr('x', mainXScale(datum)) + .raise(); } // Dispatch tooltip show event @@ -363,12 +436,26 @@ export class Heatmap extends Component { // Un-highlight all elements handleAxisMouseOut = (event: CustomEvent) => { - // Hide row/column highlighting + // Hide shadow this.parent - .selectAll('rect.highlighter-column,rect.highlighter-row') - .classed('highlighter-hidden', true) + .selectAll('rect.shadow-column,rect.shadow-row') + .classed('highlighter-hidden', true); + + // Lower cells + this.parent + .selectAll('axis-hovered') + .classed('axis-hovered', false) .lower(); + // Hide row/column highlighting + const strokeHighlighter = this.parent + .selectAll('g.row-column-highlighter') + .lower(); + + strokeHighlighter + .selectAll('rect.highlight-column, rect.highlight-row') + .classed('highlighter-hidden', true); + // Dispatch hide tooltip event this.services.events.dispatchEvent(Events.Tooltip.HIDE, { event, diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 9ad85ed07a..b5df0fb59a 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -8,14 +8,21 @@ visibility: hidden; } - rect.highlighter-column, - rect.highlighter-row { + rect.shadow-column, + rect.shadow-row { stroke: #ffffff; - fill: transparent; + fill: white; stroke-width: 2px; filter: drop-shadow(0px 0px 5px black); } + rect.highlight-column, + rect.highlight-row { + stroke: #ffffff; + fill: none; + stroke-width: 2px; + } + rect.null-state { stroke: transparent !important; } From fa4be7e09f9049cf53bd793cd179d2f469d079f0 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Sun, 28 Nov 2021 03:05:36 -0500 Subject: [PATCH 39/68] Improve hover accessibility --- .../core/src/components/axes/hover-axis.ts | 82 ++++++++++++++++--- 1 file changed, 69 insertions(+), 13 deletions(-) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 15f580ffee..5d703b9897 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -1,21 +1,12 @@ // Internal Imports import { Axis } from './axis'; -import { - AxisPositions, - Events, - ScaleTypes, - Roles, - TruncationTypes, -} from '../../interfaces'; +import { AxisPositions, Events, ScaleTypes } from '../../interfaces'; import { Tools } from '../../tools'; import { ChartModel } from '../../model/model'; import { DOMUtils } from '../../services'; -import { AxisTitleOrientations, TickRotations } from '../../interfaces/enums'; -import * as Configuration from '../../configuration'; // D3 Imports import { select } from 'd3-selection'; -import { axisBottom, axisLeft, axisRight, axisTop } from 'd3-axis'; export class HoverAxis extends Axis { constructor(model: ChartModel, services: any, configs?: any) { @@ -31,15 +22,17 @@ export class HoverAxis extends Axis { `g.axis.${axisPosition}` ); + container.selectAll('g.ticks').attr('tabindex', 0); + container.selectAll('g.tick').each(function () { const g = select(this); - g.classed('tick-hover', true).attr('tabindex', 0); + g.classed('tick-hover', true).attr('tabindex', -1); const textNode = g.select('text'); const { width, height } = DOMUtils.getSVGElementSize(textNode, { useBBox: true, }); - const rectangle = DOMUtils.appendOrSelect(g, `rect.axis-holder`); + const rectangle = DOMUtils.appendOrSelect(g, 'rect.axis-holder'); let x = 0, y = 0; @@ -72,10 +65,72 @@ export class HoverAxis extends Axis { rectangle.lower(); }); + const self = this; + + container.selectAll('g.ticks').on('focus', function () { + const axis = select(this); + this.blur(); + + if (!axis.classed('invisible')) { + // Set focus on intial value in the axis + axis.select('g.tick').dispatch('focus'); + + axis.selectAll('g.tick').on( + 'keydown', + function (event: KeyboardEvent) { + // Choose specific arrow key depending on the axis + if ( + axisPosition === AxisPositions.LEFT || + axisPosition === AxisPositions.RIGHT + ) { + if (event.key && event.key === 'ArrowUp') { + self.goNext(this as HTMLElement, event); + } else if (event.key && event.key === 'ArrowDown') { + self.goPrevious(this as HTMLElement, event); + } + } else { + if (event.key && event.key === 'ArrowLeft') { + self.goPrevious(this as HTMLElement, event); + } else if ( + event.key && + event.key === 'ArrowRight' + ) { + self.goNext(this as HTMLElement, event); + } + } + } + ); + } + }); + // Add event listeners to elements drawn this.addEventListeners(); } + // Focus on the next HTML element sibling + private goNext(element: HTMLElement, event: Event) { + if ( + element.nextElementSibling !== null && + element.nextElementSibling.tagName !== 'path' + ) { + element.nextElementSibling.dispatchEvent(new Event('focus')); + } + + event.preventDefault(); + } + + // Focus on the previous HTML element sibling + private goPrevious(element: HTMLElement, event: Event) { + if ( + element.previousElementSibling !== null && + element.previousElementSibling.tagName !== 'path' + ) { + element.previousElementSibling.dispatchEvent(new Event('focus')); + } + + event.preventDefault(); + } + addEventListeners() { const svg = this.getComponentContainer(); const { position: axisPosition } = this.configs; @@ -131,7 +186,6 @@ export class HoverAxis extends Axis { axisScaleType === ScaleTypes.LABELS && datum.length > truncationThreshold ) { - console.log('inside'); self.services.events.dispatchEvent(Events.Tooltip.MOVE, { event, }); @@ -162,6 +216,8 @@ export class HoverAxis extends Axis { container .selectAll('g.tick.tick-hover') .on('focus', function (event) { + // Focus element since we are using arrow keys + event.target.focus(); // Dispatch mouse event self.services.events.dispatchEvent( Events.Axis.LABEL_MOUSEOVER, From 7ef47974cd7d226795639c72a31fa4c0be1dd171 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Sun, 28 Nov 2021 03:07:07 -0500 Subject: [PATCH 40/68] Dispatch correct events --- packages/core/src/components/axes/hover-axis.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 5d703b9897..1b7f99ef7a 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -220,7 +220,7 @@ export class HoverAxis extends Axis { event.target.focus(); // Dispatch mouse event self.services.events.dispatchEvent( - Events.Axis.LABEL_MOUSEOVER, + Events.Axis.LABEL_FOCUS, { event, element: select(this), @@ -230,7 +230,7 @@ export class HoverAxis extends Axis { }) .on('blur', function (event) { // Dispatch mouse event - self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, { + self.services.events.dispatchEvent(Events.Axis.LABEL_BLUR, { event, element: select(this), datum: select(this).select('text').datum(), From 1fed27e8346ab87c29b70cdca870becb2d275719 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:13:06 -0500 Subject: [PATCH 41/68] Add gradient colors to css --- packages/core/src/styles/colors.scss | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/core/src/styles/colors.scss b/packages/core/src/styles/colors.scss index 3d96a5d6eb..7f8be8b856 100644 --- a/packages/core/src/styles/colors.scss +++ b/packages/core/src/styles/colors.scss @@ -102,6 +102,10 @@ .stroke-#{$token} { stroke: $color; } + + .stop-color-#{$token} { + stop-color: $color; + } } } From 4ca43003a5265426d64c92271818ade241aee5ce Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:13:56 -0500 Subject: [PATCH 42/68] Add axis render event and missing focus and blur --- packages/core/src/components/axes/hover-axis.ts | 13 +++++-------- .../components/axes/two-dimensional-hover-axes.ts | 2 ++ packages/core/src/interfaces/events.ts | 3 +++ 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 1b7f99ef7a..f79e0c31ed 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -219,14 +219,11 @@ export class HoverAxis extends Axis { // Focus element since we are using arrow keys event.target.focus(); // Dispatch mouse event - self.services.events.dispatchEvent( - Events.Axis.LABEL_FOCUS, - { - event, - element: select(this), - datum: select(this).select('text').datum(), - } - ); + self.services.events.dispatchEvent(Events.Axis.LABEL_FOCUS, { + event, + element: select(this), + datum: select(this).select('text').datum(), + }); }) .on('blur', function (event) { // Dispatch mouse event diff --git a/packages/core/src/components/axes/two-dimensional-hover-axes.ts b/packages/core/src/components/axes/two-dimensional-hover-axes.ts index e458f2c135..e9d70a590a 100644 --- a/packages/core/src/components/axes/two-dimensional-hover-axes.ts +++ b/packages/core/src/components/axes/two-dimensional-hover-axes.ts @@ -98,6 +98,8 @@ export class TwoDimensionalHoverAxes extends TwoDimensionalAxes { } }); + this.services.events.dispatchEvent(Events.Axis.RENDER_COMPLETE); + // If the new margins are different than the existing ones const isNotEqual = Object.keys(margins).some((marginKey) => { return this.margins[marginKey] !== margins[marginKey]; diff --git a/packages/core/src/interfaces/events.ts b/packages/core/src/interfaces/events.ts index 257001a208..4776e7b376 100644 --- a/packages/core/src/interfaces/events.ts +++ b/packages/core/src/interfaces/events.ts @@ -65,6 +65,9 @@ export enum Axis { LABEL_MOUSEMOVE = 'axis-label-mousemove', LABEL_CLICK = 'axis-label-click', LABEL_MOUSEOUT = 'axis-label-mouseout', + LABEL_FOCUS = 'axis-label-focus', + LABEL_BLUR = 'axis-label-blur', + RENDER_COMPLETE = 'axis-render-complete', } /** From de64f46bb26718d4ca87e9c77abc49ae47f067a7 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:14:40 -0500 Subject: [PATCH 43/68] Remove linear gradient from model --- packages/core/src/model/heatmap.ts | 190 +++-------------------------- 1 file changed, 18 insertions(+), 172 deletions(-) diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 7385a0bb2a..8f480ed437 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -5,113 +5,10 @@ import { Tools } from '../tools'; // d3 imports import { extent } from 'd3-array'; -import { scaleLinear, scaleQuantize } from 'd3-scale'; +import { scaleQuantize } from 'd3-scale'; /** The gauge chart model layer */ export class HeatmapModel extends ChartModelCartesian { - private palettes = { - // Monochromatic palettes - // White, Purple 10 - 100 - 'mono-1': [ - '#ffffff', - '#f6f2ff', - '#e8daff', - '#d4bbff', - '#be95ff', - '#a56eff', - '#8a3ffc', - '#6929c4', - '#491d8b', - '#31135e', - '#1c0f30', - ], - // White, Blue 10 - 100 - 'mono-2': [ - '#ffffff', - '#edf5ff', - '#d0e2ff', - '#a6c8ff', - '#78a9ff', - '#4589ff', - '#0f62fe', - '#0043ce', - '#002d9c', - '#001d6c', - '#001141', - ], - // White, Cyan 10 - 100 - 'mono-3': [ - '#ffffff', - '#e5f6ff', - '#bae6ff', - '#82cfff', - '#33b1ff', - '#1192e8', - '#0072c3', - '#00539a', - '#003a6d', - '#012749', - '#1c0f30', - ], - // White, Teal 10 - 100 - 'mono-4': [ - '#ffffff', - '#d9fbfb', - '#9ef0f0', - '#3ddbd9', - '#08bdba', - '#009d9a', - '#007d79', - '#005d5d', - '#004144', - '#022b30', - '#081a1c', - ], - // Diverging palettes - // Red 80 - 10, White, Cyan 10 - 80 - 'diverge-1': [ - '#750e13', - '#a2191f', - '#da1e28', - '#fa4d56', - '#ff8389', - '#ffb3b8', - '#ffd7d9', - '#fff1f1', - '#ffffff', - '#e5f6ff', - '#bae6ff', - '#82cfff', - '#33b1ff', - '#1192e8', - '#0072c3', - '#00539a', - '#003a6d', - ], - // Purple 80 - 10, Teal 10 - 80 - 'diverge-2': [ - '#491d8b', - '#6929c4', - '#8a3ffc', - '#a56eff', - '#be95ff', - '#d4bbff', - '#e8daff', - '#f6f2ff', - '#fff1f1', - '#ffffff', - '#d9fbfb', - '#9ef0f0', - '#3ddbd9', - '#08bdba', - '#009d9a', - '#007d79', - '#005d5d', - '#004144', - ], - }; - - private colorScaleType: ColorLegendType = ColorLegendType.LINEAR; selectedPalette = []; private _colorScale: any = undefined; @@ -386,13 +283,6 @@ export class HeatmapModel extends ChartModelCartesian { // Uses quantize scale to return class names getColorClassName(configs: { value?: number; originalClassName?: string }) { - if ( - typeof configs.value === 'number' && - this.colorScaleType !== ColorLegendType.QUANTIZE - ) { - return configs.originalClassName; - } - return `${configs.originalClassName} ${this._colorScale( configs.value as number )}`; @@ -416,13 +306,6 @@ export class HeatmapModel extends ChartModelCartesian { 'option' ); - const colorScaleType = Tools.getProperty( - options, - 'legend', - 'colorLegend', - 'type' - ); - // If domain consists of negative and positive values, use diverging palettes const domain = this.getValueDomain(); const colorScheme = domain[0] < 0 && domain[1] > 0 ? 'diverge' : 'mono'; @@ -442,61 +325,24 @@ export class HeatmapModel extends ChartModelCartesian { colorPairingOption = 1; } - // Define color scale based on legend - if (colorScaleType === ColorLegendType.LINEAR) { - // Uses hardcoded fill on element - this.colorScaleType = ColorLegendType.LINEAR; - - const colorPairing = customColorsEnabled ? customColors : []; - let ticks = []; - - // Use only the first, middle, and last color to determine the color gradient - // since they are the only displayed ticks - if (!customColorsEnabled) { - const palette = this.palettes[ - `${colorScheme}-${colorPairingOption}` - ]; - colorPairing.push(palette[0]); - colorPairing.push(palette[Math.floor(palette.length / 2)]); - colorPairing.push(palette[palette.length - 1]); - ticks = - colorScheme === 'diverge' - ? [domain[0], 0, domain[1]] - : [domain[0], domain[1] / 2, domain[1]]; - } else { - ticks = scaleLinear() - .domain([domain[0], domain[1]]) - .nice() - .ticks(colorPairing.length); - } - - // Save scale type - this.selectedPalette = colorPairing; - this._colorScale = scaleLinear().domain(ticks).range(colorPairing); - } else if (colorScaleType === ColorLegendType.QUANTIZE) { - // Uses css classes for fill - this.colorScaleType = ColorLegendType.QUANTIZE; - - const colorPairing = customColorsEnabled ? customColors : []; - - if (!customColorsEnabled) { - // Add class names to list and the amount based on the color scheme - // Carbon charts has 11 colors for a single monochromatic palette & 17 for a divergent palette - const colorGroupingLength = colorScheme === 'diverge' ? 17 : 11; - for (let i = 1; i < colorGroupingLength + 1; i++) { - colorPairing.push( - `fill-${colorScheme}-${colorPairingOption}-${i}` - ); - } + // Uses css classes for fill + const colorPairing = customColorsEnabled ? customColors : []; + + if (!customColorsEnabled) { + // Add class names to list and the amount based on the color scheme + // Carbon charts has 11 colors for a single monochromatic palette & 17 for a divergent palette + const colorGroupingLength = colorScheme === 'diverge' ? 17 : 11; + for (let i = 1; i < colorGroupingLength + 1; i++) { + colorPairing.push( + `fill-${colorScheme}-${colorPairingOption}-${i}` + ); } - - // Save scale type - this.selectedPalette = colorPairing; - this._colorScale = scaleQuantize() - .domain(this.getValueDomain() as [number, number]) - .range(colorPairing); - } else { - throw Error(`Color legend ${colorScaleType} is not supported`); } + + // Save scale type + this.selectedPalette = colorPairing; + this._colorScale = scaleQuantize() + .domain(this.getValueDomain() as [number, number]) + .range(colorPairing); } } From e2ee256f8126a37127fda503b2aa0cba0bddc905 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:15:38 -0500 Subject: [PATCH 44/68] Make color scale legend more independent from class model --- .../essentials/color-scale-legend.ts | 176 ++++++++++++++---- 1 file changed, 141 insertions(+), 35 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index f1dd30d020..8d4c0e5651 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -1,11 +1,6 @@ // Internal Imports import { Tools } from '../../tools'; -import { - RenderTypes, - Roles, - TruncationTypes, - ColorLegendType, -} from '../../interfaces'; +import { ColorLegendType, Events, RenderTypes, Roles } from '../../interfaces'; import * as Configuration from '../../configuration'; // D3 imports @@ -21,6 +16,31 @@ export class ColorScaleLegend extends Legend { private gradient_id = 'gradient-id-' + Math.floor(Math.random() * 99999999999); + private dimensions = []; + + init() { + console.log('inside here 2 - init'); + const eventsFragment = this.services.events; + + // Highlight correct circle on legend item hovers + eventsFragment.addEventListener( + Events.Axis.RENDER_COMPLETE, + this.handleAxisComplete + ); + } + + handleAxisComplete = (event: CustomEvent) => { + const { cartesianScales } = this.services; + // Get available chart area + const mainXScale = cartesianScales.getMainXScale(); + const mainYScale = cartesianScales.getMainYScale(); + + this.dimensions = [mainXScale.range(), mainYScale.range()]; + + // Move the legend to the right + // Add the text + }; + render() { const options = this.getOptions(); @@ -36,25 +56,76 @@ export class ColorScaleLegend extends Legend { ); const customColorsEnabled = !Tools.isEmpty(customColors); - const palette = this.model.selectedPalette; const domain = this.model.getValueDomain(); + const colorScaleType = Tools.getProperty( + options, + 'legend', + 'colorLegend', + 'type' + ); + + let colorPairingOption = Tools.getProperty( + options, + 'color', + 'pairing', + 'option' + ); + const group = svg.append('g'); - if (this.model.colorScaleType === ColorLegendType.LINEAR) { - const stopLengthPercentage = 100 / (palette.length - 1); + // If domain consists of negative and positive values, use diverging palettes + const colorScheme = domain[0] < 0 && domain[1] > 0 ? 'diverge' : 'mono'; + + // Use default color pairing options if not in defined range + if ( + colorPairingOption < 1 && + colorPairingOption > 4 && + colorScheme === 'mono' + ) { + colorPairingOption = 1; + } else if ( + colorPairingOption < 1 && + colorPairingOption > 2 && + colorScheme === 'diverge' + ) { + colorPairingOption = 1; + } + + let colorPairing = []; + // Carbon charts has 11 colors for a single monochromatic palette & 17 for a divergent palette + let colorGroupingLength = colorScheme === 'diverge' ? 17 : 11; + + if (!customColorsEnabled) { + // Add class names to list and the amount based on the color scheme + for (let i = 1; i < colorGroupingLength + 1; i++) { + colorPairing.push( + colorScaleType === ColorLegendType.LINEAR + ? `stop-color-${colorScheme}-${colorPairingOption}-${i}` + : `fill-${colorScheme}-${colorPairingOption}-${i}` + ); + } + } else { + // Use custom colors + colorPairing = customColors; + } + + if (colorScaleType === ColorLegendType.LINEAR) { + const stopLengthPercentage = 100 / (colorPairing.length - 1); // Generate the gradient const linearGradient = group .append('linearGradient') .attr('id', `${this.gradient_id}-legend`) .selectAll('stop') - .data(palette) + .data(colorPairing) .enter() .append('stop') .attr('offset', (_, i) => `${i * stopLengthPercentage}%`) + .attr('class', (_, i) => colorPairing[i]) .attr('stop-color', (d) => d); + // Create the legend container const rectangle = group .append('rect') .attr('width', Configuration.legend.color.barWidth) @@ -85,57 +156,92 @@ export class ColorScaleLegend extends Legend { // Align text to fit in container axis.style('text-anchor', 'start'); - } else if (this.model.colorScaleType === ColorLegendType.QUANTIZE) { - const colorScaleBand = scaleBand() - .domain(palette) - .rangeRound([0, Configuration.legend.color.barWidth]); - + } else if (colorScaleType === ColorLegendType.QUANTIZE) { // Generate equal chunks between range to act as ticks const interpolator = interpolateRound(domain[0], domain[1]); - const quant = quantize(interpolator, palette.length); + const quant = quantize(interpolator, colorPairing.length); - const rect = group + // Remove white if divergent is used + const other = colorPairing; + // If divergent && non-custom color, remove 0/white from being displayed + if (!customColorsEnabled && colorScheme === 'diverge') { + colorPairing.splice(colorPairing.length / 2, 1); + } + + const colorScaleBand = scaleBand() + .domain(colorPairing) + .rangeRound([0, Configuration.legend.color.barWidth]); + + const rectangle = group .selectAll('rect') .data(colorScaleBand.domain()) .join('rect') .attr('x', colorScaleBand) .attr('y', 0) .attr('width', Math.max(0, colorScaleBand.bandwidth() - 1)) - .attr('height', Configuration.legend.color.barHeight); - - // Use attribute fill or css depending on custom Colors - if (customColorsEnabled) { - rect.attr('fill', (_, i) => { - return palette[i]; - }); - } else { - rect.attr('class', (_, i) => { - return palette[i]; - }); - } + .attr('height', Configuration.legend.color.barHeight) + .attr('class', (d) => d) + .attr('fill', (d) => d); const xAxis = axisBottom(colorScaleBand) .tickSize(0) - .tickValues(palette) + .tickValues(colorPairing) .tickFormat((_, i) => { + // Display every other tick to create space + if ( + !customColorsEnabled && + ((i + 1) % 2 === 0 || i === colorPairing.length - 1) + ) { + console.log('tick formating', i + 1, quant[i]); + return null; + } + // Use the quant interpolators as ticks return quant[i].toString(); }); // Align axis to match bandwidth start after initial (white) - group + const legendAxis = group .append('g') + .classed('legend-axis', true) .attr( 'transform', - `translate(${colorScaleBand.bandwidth() / 2}, ${ + `translate(${ + !customColorsEnabled && colorScheme === 'diverge' + ? '-' + : '' + }${colorScaleBand.bandwidth() / 2}, ${ Configuration.legend.color.axisYTranslation })` ) - .call(xAxis) - .select('.domain') - .remove(); + .call(xAxis); + + const firstTick = legendAxis.select('g.tick').clone(true); + firstTick + .attr( + 'transform', + `translate(${Configuration.legend.color.barWidth}, 0)` + ) + .classed('final-tick', true) + .select('text') + .text(quant[quant.length - 1]); + + legendAxis.enter().append(firstTick.node()).raise(); + + legendAxis.select('.domain').remove(); + + console.log('legendAxis', legendAxis); } else { throw Error('Entered color legend type is not supported.'); } } + + destroy() { + // Remove legend listeners + const eventsFragment = this.services.events; + eventsFragment.removeEventListener( + Events.Axis.RENDER_COMPLETE, + this.handleAxisComplete + ); + } } From 22c71866d2549f3afc0285b856b975481c497ae0 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:17:25 -0500 Subject: [PATCH 45/68] Add divider off demo to axis heatmap demo --- packages/core/demo/data/heatmap.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index 5cd19e1431..872b3455c0 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -637,7 +637,7 @@ export const heatmapLegendOptions = { }; export const heatmapDomainOptions = { - title: 'Heatmap (Axis order option)', + title: 'Heatmap (Axis order option & no divider)', axes: { bottom: { title: 'Letters', @@ -664,6 +664,11 @@ export const heatmapDomainOptions = { ], }, }, + heatmap: { + divider: { + state: 'off', + }, + }, }; export const heatmapMissingData = [ From 9fbbe3effec165837d8c532ed0924d4f27409298 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:55:11 -0500 Subject: [PATCH 46/68] Update spacing between axis & legend --- packages/core/src/charts/heatmap.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index 7c06361608..a286825ff0 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -114,7 +114,7 @@ export class HeatmapChart extends AxisChart { const legendSpacerComponent = { id: 'spacer', - components: [new Spacer(this.model, this.services, { size: 8 })], + components: [new Spacer(this.model, this.services, { size: 15 })], growth: LayoutGrowth.PREFERRED, }; From ba2cd293d902b564d99e403169cb7c0e182d904a Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 01:56:22 -0500 Subject: [PATCH 47/68] Adjust legend position after axis loads --- .../essentials/color-scale-legend.ts | 25 ++++++++++--------- .../src/styles/components/_color-legend.scss | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 8d4c0e5651..6cc0f9e9b9 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -16,8 +16,6 @@ export class ColorScaleLegend extends Legend { private gradient_id = 'gradient-id-' + Math.floor(Math.random() * 99999999999); - private dimensions = []; - init() { console.log('inside here 2 - init'); const eventsFragment = this.services.events; @@ -35,10 +33,15 @@ export class ColorScaleLegend extends Legend { const mainXScale = cartesianScales.getMainXScale(); const mainYScale = cartesianScales.getMainYScale(); - this.dimensions = [mainXScale.range(), mainYScale.range()]; + const xDimensions = mainXScale.range(); + + // Align legend with the axis + if (xDimensions[0] > 1) { + const svg = this.getComponentContainer(); + svg.select('g.legend-rectangle').attr('transform', `translate(${xDimensions[0]}, 0)`) + } - // Move the legend to the right - // Add the text + // @todo - Add the text }; render() { @@ -72,7 +75,7 @@ export class ColorScaleLegend extends Legend { 'option' ); - const group = svg.append('g'); + const group = svg.append('g').classed('legend-rectangle', true); // If domain consists of negative and positive values, use diverging palettes const colorScheme = domain[0] < 0 && domain[1] > 0 ? 'diverge' : 'mono'; @@ -206,12 +209,10 @@ export class ColorScaleLegend extends Legend { .classed('legend-axis', true) .attr( 'transform', - `translate(${ - !customColorsEnabled && colorScheme === 'diverge' - ? '-' - : '' - }${colorScaleBand.bandwidth() / 2}, ${ - Configuration.legend.color.axisYTranslation + `translate(${!customColorsEnabled && colorScheme === 'diverge' + ? '-' + : '' + }${colorScaleBand.bandwidth() / 2}, ${Configuration.legend.color.axisYTranslation })` ) .call(xAxis); diff --git a/packages/core/src/styles/components/_color-legend.scss b/packages/core/src/styles/components/_color-legend.scss index e273b4a9a5..de9d02158b 100644 --- a/packages/core/src/styles/components/_color-legend.scss +++ b/packages/core/src/styles/components/_color-legend.scss @@ -1,5 +1,5 @@ svg.#{$prefix}--#{$charts-prefix}--color-legend { display: flex; user-select: none; - height: 28px; + height: 38px; } From 741052b6fb97a59580a6b39f95d37172fc7a6893 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 02:43:10 -0500 Subject: [PATCH 48/68] hover bug fixes --- .../core/src/components/graphs/heatmap.ts | 27 ++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index ac7736ddfd..82925a91de 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -20,17 +20,29 @@ export class Heatmap extends Component { init() { const eventsFragment = this.services.events; - // Highlight correct circle on legend item hovers + // Highlight correct cells on Axis item hovers eventsFragment.addEventListener( Events.Axis.LABEL_MOUSEOVER, this.handleAxisOnHover ); - // Un-highlight circles on legend item mouseouts + // Highlight correct cells on Axis item mouseouts eventsFragment.addEventListener( Events.Axis.LABEL_MOUSEOUT, this.handleAxisMouseOut ); + + // Highlight correct cells on Axis item focus + eventsFragment.addEventListener( + Events.Axis.LABEL_FOCUS, + this.handleAxisOnHover + ); + + // Highlight correct cells on Axis item blur + eventsFragment.addEventListener( + Events.Axis.LABEL_BLUR, + this.handleAxisMouseOut + ); } render(animate = true) { @@ -344,7 +356,7 @@ export class Heatmap extends Component { const ids = []; // Check to see where datum belongs - if (this.matrix[datum] != undefined) { + if (this.matrix[datum] !== undefined) { label = domainLabel; // Iterate through Object and get sum, min, and max ranges.forEach((element) => { @@ -364,7 +376,7 @@ export class Heatmap extends Component { sum += value; min = value < min ? value : min; max = value > max ? value : max; - const id = this.matrix[datum][element].index; + const id = this.matrix[element][datum].index; if (id >= 0) { ids.push(`g.heat-${id}`); } @@ -382,7 +394,7 @@ export class Heatmap extends Component { .selectAll('g.row-column-highlighter') .raise(); - if (mainXScale(datum)) { + if (mainXScale(datum) !== undefined) { this.parent .select('rect.shadow-column') .classed('highlighter-hidden', false) @@ -394,7 +406,8 @@ export class Heatmap extends Component { .classed('highlighter-hidden', false) .attr('x', mainXScale(datum)) .raise(); - } else if (mainYScale(datum)) { + } else if (mainYScale(datum) !== undefined) { + this.parent .select('rect.shadow-row') .classed('highlighter-hidden', false) @@ -404,7 +417,7 @@ export class Heatmap extends Component { strokeHighlighter .select('rect.highlight-row') .classed('highlighter-hidden', false) - .attr('x', mainXScale(datum)) + .attr('y', mainYScale(datum)) .raise(); } From 7e1bfcc551d6993c14feb294c902692a9bfe58d4 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 29 Nov 2021 02:44:21 -0500 Subject: [PATCH 49/68] format --- .../components/essentials/color-scale-legend.ts | 15 ++++++++++----- packages/core/src/components/graphs/heatmap.ts | 1 - 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 6cc0f9e9b9..b741e3c571 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -38,7 +38,10 @@ export class ColorScaleLegend extends Legend { // Align legend with the axis if (xDimensions[0] > 1) { const svg = this.getComponentContainer(); - svg.select('g.legend-rectangle').attr('transform', `translate(${xDimensions[0]}, 0)`) + svg.select('g.legend-rectangle').attr( + 'transform', + `translate(${xDimensions[0]}, 0)` + ); } // @todo - Add the text @@ -209,10 +212,12 @@ export class ColorScaleLegend extends Legend { .classed('legend-axis', true) .attr( 'transform', - `translate(${!customColorsEnabled && colorScheme === 'diverge' - ? '-' - : '' - }${colorScaleBand.bandwidth() / 2}, ${Configuration.legend.color.axisYTranslation + `translate(${ + !customColorsEnabled && colorScheme === 'diverge' + ? '-' + : '' + }${colorScaleBand.bandwidth() / 2}, ${ + Configuration.legend.color.axisYTranslation })` ) .call(xAxis); diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 82925a91de..29b15d8e71 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -407,7 +407,6 @@ export class Heatmap extends Component { .attr('x', mainXScale(datum)) .raise(); } else if (mainYScale(datum) !== undefined) { - this.parent .select('rect.shadow-row') .classed('highlighter-hidden', false) From 429614353937d2bee55ef543d8d5a95ed6219d42 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 30 Nov 2021 00:57:14 -0500 Subject: [PATCH 50/68] Move row and column highlighting outside of chart --- .../core/src/components/graphs/heatmap.ts | 84 ++++++++++++------- .../src/configuration-non-customizable.ts | 2 + 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 29b15d8e71..f1014e0c74 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -1,8 +1,14 @@ // Internal Imports import { Component } from '../component'; import * as Configuration from '../../configuration'; -import { Events, RenderTypes, DividerStatus } from '../../interfaces'; +import { + Events, + RenderTypes, + DividerStatus, + AxisPositions, +} from '../../interfaces'; import { Tools } from '../../tools'; +import { DOMUtils } from '../../services'; import { get } from 'lodash-es'; @@ -16,6 +22,10 @@ export class Heatmap extends Component { private matrix = {}; private xBandwidth = 0; private yBandwidth = 0; + private translationUnits = { + x: 0, + y: 0, + }; init() { const eventsFragment = this.services.events; @@ -79,12 +89,36 @@ export class Heatmap extends Component { (yRange[1] - yRange[0]) / uniqueRange.length ); - const dividerStatus = this.determineDividerStatus(); + // Add padding around chart so the axes lines display + const axesOptions = Tools.getProperty(this.getOptions(), 'axes'); + if (axesOptions) { + Object.keys(axesOptions).forEach((axisPosition) => { + switch (axisPosition) { + case AxisPositions.TOP: + this.translationUnits[1] = + Configuration.heatmap.chartPadding; + break; + case AxisPositions.BOTTOM: + this.translationUnits[1] = -Configuration.heatmap + .chartPadding; + break; + case AxisPositions.LEFT: + this.translationUnits[0] = + Configuration.heatmap.chartPadding; + break; + case AxisPositions.RIGHT: + this.translationUnits[0] = -Configuration.heatmap + .chartPadding; + break; + } + }); + } + const container = svg .append('g') .attr( 'transform', - dividerStatus ? `translate(1, -1)` : `translate(0, 0)` + `translate(${this.translationUnits[0]}, ${this.translationUnits[1]})` ); const rectangles = container @@ -148,12 +182,12 @@ export class Heatmap extends Component { .attr('width', this.xBandwidth) .attr('height', Math.abs(yRange[1] - yRange[0])); - const rowAndColumnHighlighter = container - .append('g') - .classed('row-column-highlighter', true); + const parent = DOMUtils.appendOrSelect( + this.parent, + 'g.row-column-highlighter' + ); - rowAndColumnHighlighter - .append('rect') + DOMUtils.appendOrSelect(parent, 'rect.highlight-row') .classed('highlighter-hidden', true) .classed('highlight-row', true) .attr('x', xRange[0]) @@ -161,10 +195,8 @@ export class Heatmap extends Component { .attr('width', Math.abs(xRange[1] - xRange[0])) .attr('height', this.yBandwidth); - rowAndColumnHighlighter - .append('rect') + DOMUtils.appendOrSelect(parent, 'rect.highlight-column') .classed('highlighter-hidden', true) - .classed('highlight-column', true) .attr('x', xRange[0]) .attr('y', 0) .attr('width', this.xBandwidth) @@ -383,40 +415,39 @@ export class Heatmap extends Component { }); } + // Show the outlines to make the cells appear grouped + const shadowHighlighter = this.parent.select('g.shadow-holder').raise(); // Pop out cells this.parent .selectAll(ids.join(',')) .classed('axis-hovered', true) .raise(); - // Show the outlines to make the cells appear grouped - const strokeHighlighter = this.parent - .selectAll('g.row-column-highlighter') - .raise(); - if (mainXScale(datum) !== undefined) { - this.parent + shadowHighlighter .select('rect.shadow-column') .classed('highlighter-hidden', false) - .attr('x', mainXScale(datum)) + .attr('x', mainXScale(datum) + this.translationUnits[0]) .raise(); - strokeHighlighter + this.parent + .select('g.row-column-highlighter') .select('rect.highlight-column') .classed('highlighter-hidden', false) - .attr('x', mainXScale(datum)) + .attr('x', mainXScale(datum) + this.translationUnits[0]) .raise(); } else if (mainYScale(datum) !== undefined) { - this.parent + shadowHighlighter .select('rect.shadow-row') .classed('highlighter-hidden', false) - .attr('y', mainYScale(datum)) + .attr('y', mainXScale(datum) + this.translationUnits[1]) .raise(); - strokeHighlighter + this.parent + .select('g.row-column-highlighter') .select('rect.highlight-row') .classed('highlighter-hidden', false) - .attr('y', mainYScale(datum)) + .attr('y', mainXScale(datum) + this.translationUnits[1]) .raise(); } @@ -455,16 +486,13 @@ export class Heatmap extends Component { // Lower cells this.parent - .selectAll('axis-hovered') + .selectAll('.axis-hovered') .classed('axis-hovered', false) .lower(); // Hide row/column highlighting const strokeHighlighter = this.parent .selectAll('g.row-column-highlighter') - .lower(); - - strokeHighlighter .selectAll('rect.highlight-column, rect.highlight-row') .classed('highlighter-hidden', true); diff --git a/packages/core/src/configuration-non-customizable.ts b/packages/core/src/configuration-non-customizable.ts index e44462c4e3..1fe056f082 100644 --- a/packages/core/src/configuration-non-customizable.ts +++ b/packages/core/src/configuration-non-customizable.ts @@ -212,6 +212,8 @@ export const alluvial = { export const heatmap = { minCellDividerDimension: 16, + // Ensures axes lines are displayed with or without stroke disabled + chartPadding: 0.5, }; export const spacers = { From 25ccd1599264f0056c4e492c007662c265cba979 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 30 Nov 2021 01:02:56 -0500 Subject: [PATCH 51/68] format --- packages/core/src/charts/heatmap.ts | 2 +- .../core/src/components/axes/two-dimensional-hover-axes.ts | 3 +-- packages/core/src/components/essentials/color-scale-legend.ts | 4 ---- packages/core/src/model/heatmap.ts | 2 +- 4 files changed, 3 insertions(+), 8 deletions(-) diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index a286825ff0..740c3e1a58 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -50,7 +50,7 @@ export class HeatmapChart extends AxisChart { } // Custom getChartComponents - Implements getChartComponents - // Removes zoombar support and additional `features` that are not supported y heatmap + // Removes zoombar support and additional `features` that are not supported in heatmap private getChartComponentsList(graphFrameComponents: any[], configs?: any) { const options = this.model.getOptions(); const toolbarEnabled = Tools.getProperty(options, 'toolbar', 'enabled'); diff --git a/packages/core/src/components/axes/two-dimensional-hover-axes.ts b/packages/core/src/components/axes/two-dimensional-hover-axes.ts index e9d70a590a..90b4ee19c4 100644 --- a/packages/core/src/components/axes/two-dimensional-hover-axes.ts +++ b/packages/core/src/components/axes/two-dimensional-hover-axes.ts @@ -1,9 +1,8 @@ // Internal Imports import { TwoDimensionalAxes } from './two-dimensional-axes'; -import { AxisPositions, RenderTypes } from '../../interfaces'; +import { AxisPositions } from '../../interfaces'; import { Tools } from '../../tools'; import { DOMUtils } from '../../services'; -import { Threshold } from '../essentials/threshold'; import { Events } from './../../interfaces'; import { HoverAxis } from './hover-axis'; diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index b741e3c571..9f80bf566c 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -17,7 +17,6 @@ export class ColorScaleLegend extends Legend { 'gradient-id-' + Math.floor(Math.random() * 99999999999); init() { - console.log('inside here 2 - init'); const eventsFragment = this.services.events; // Highlight correct circle on legend item hovers @@ -198,7 +197,6 @@ export class ColorScaleLegend extends Legend { !customColorsEnabled && ((i + 1) % 2 === 0 || i === colorPairing.length - 1) ) { - console.log('tick formating', i + 1, quant[i]); return null; } @@ -235,8 +233,6 @@ export class ColorScaleLegend extends Legend { legendAxis.enter().append(firstTick.node()).raise(); legendAxis.select('.domain').remove(); - - console.log('legendAxis', legendAxis); } else { throw Error('Entered color legend type is not supported.'); } diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 8f480ed437..a77a2bbbb5 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -1,5 +1,5 @@ // Internal Imports -import { ColorLegendType, ScaleTypes } from '../interfaces'; +import { ScaleTypes } from '../interfaces'; import { ChartModelCartesian } from './cartesian-charts'; import { Tools } from '../tools'; From ea6dbe4e6bee6b15935113f969ee2c6b3dc3b922 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 30 Nov 2021 02:32:03 -0500 Subject: [PATCH 52/68] Align legend title with axis --- packages/core/demo/data/heatmap.ts | 2 +- .../essentials/color-scale-legend.ts | 77 ++++++++++++++++--- packages/core/src/interfaces/components.ts | 5 ++ .../src/styles/components/_color-legend.scss | 20 +++++ 4 files changed, 91 insertions(+), 13 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index 872b3455c0..cecb703d8e 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -632,7 +632,7 @@ export const heatmapLegendOptions = { }, }, legend: { - colorLegend: { type: 'quantize' }, + colorLegend: { title: 'Legend title', type: 'quantize' }, }, }; diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 9f80bf566c..57cdb6d9d2 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -2,10 +2,11 @@ import { Tools } from '../../tools'; import { ColorLegendType, Events, RenderTypes, Roles } from '../../interfaces'; import * as Configuration from '../../configuration'; +import { Legend } from '../'; +import { DOMUtils } from '../../services'; // D3 imports import { axisBottom } from 'd3-axis'; -import { Legend } from '..'; import { scaleBand, scaleLinear } from 'd3-scale'; import { interpolateRound, quantize } from 'd3-interpolate'; @@ -16,6 +17,8 @@ export class ColorScaleLegend extends Legend { private gradient_id = 'gradient-id-' + Math.floor(Math.random() * 99999999999); + private textWidth = 0; + init() { const eventsFragment = this.services.events; @@ -27,41 +30,71 @@ export class ColorScaleLegend extends Legend { } handleAxisComplete = (event: CustomEvent) => { + const svg = this.getComponentContainer(); + + const title = Tools.getProperty( + this.getOptions(), + 'legend', + 'colorLegend', + 'title' + ); + const { cartesianScales } = this.services; // Get available chart area const mainXScale = cartesianScales.getMainXScale(); - const mainYScale = cartesianScales.getMainYScale(); const xDimensions = mainXScale.range(); // Align legend with the axis if (xDimensions[0] > 1) { - const svg = this.getComponentContainer(); svg.select('g.legend-rectangle').attr( 'transform', `translate(${xDimensions[0]}, 0)` ); - } - // @todo - Add the text + if (title) { + const { + width: textWidth, + } = DOMUtils.getSVGElementSize( + svg.select('g.legend-title').select('text'), + { useBBox: true } + ); + + // -9 since LEFT y-axis labels are moved towards the left by 9 + const availableSpace = xDimensions[0] - textWidth - 9; + + // If space is available align the the label with the axis labels + if (availableSpace > 1) { + svg.select('g.legend-title').attr( + 'transform', + `translate(${availableSpace}, 0)` + ); + } else { + // Move the legend down by 16 pixels to display legend text on top + svg.select('g.legend-rectangle').attr( + 'transform', + `translate(${xDimensions[0]}, 16)` + ); + + // Align legend title with start of axis + svg.select('g.legend-title').attr( + 'transform', + `translate(${xDimensions[0]}, 0)` + ); + } + } + } }; render() { const options = this.getOptions(); - // svg and container widths - const svg = this.getComponentContainer(); - svg.html('').attr('role', Roles.GROUP); - const customColors = Tools.getProperty( options, 'color', 'gradient', 'colors' ); - const customColorsEnabled = !Tools.isEmpty(customColors); - - const domain = this.model.getValueDomain(); const colorScaleType = Tools.getProperty( options, @@ -77,8 +110,28 @@ export class ColorScaleLegend extends Legend { 'option' ); + const title = Tools.getProperty( + options, + 'legend', + 'colorLegend', + 'title' + ); + + const customColorsEnabled = !Tools.isEmpty(customColors); + const domain = this.model.getValueDomain(); + + const svg = this.getComponentContainer(); + svg.html('').attr('role', Roles.GROUP); const group = svg.append('g').classed('legend-rectangle', true); + if (title) { + svg.append('g') + .classed('legend-title', true) + .append('text') + .text(title) + .attr('dy', '0.7em'); + } + // If domain consists of negative and positive values, use diverging palettes const colorScheme = domain[0] < 0 && domain[1] > 0 ? 'diverge' : 'mono'; diff --git a/packages/core/src/interfaces/components.ts b/packages/core/src/interfaces/components.ts index 820b37ed78..b930449b89 100644 --- a/packages/core/src/interfaces/components.ts +++ b/packages/core/src/interfaces/components.ts @@ -50,6 +50,11 @@ export interface LegendOptions { * enabled by default on select charts */ colorLegend?: { + /** + * Text to display beside or on top of the legend + * Position is determined by text length + */ + title?: string; type: ColorLegendType; }; } diff --git a/packages/core/src/styles/components/_color-legend.scss b/packages/core/src/styles/components/_color-legend.scss index de9d02158b..4b952bdf34 100644 --- a/packages/core/src/styles/components/_color-legend.scss +++ b/packages/core/src/styles/components/_color-legend.scss @@ -2,4 +2,24 @@ svg.#{$prefix}--#{$charts-prefix}--color-legend { display: flex; user-select: none; height: 38px; + + @if $carbon--theme == + $carbon--theme--g90 or + $carbon--theme == + $carbon--theme--g100 + { + g.legend-title text { + color: white; + } + } + + @if $carbon--theme == + $carbon--theme--g10 or + $carbon--theme == + $carbon--theme--white + { + g.legend-title text { + fill: black; + } + } } From f86e7ce8067ee9fcf50c0372313e2104337b9260 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 1 Dec 2021 01:34:34 -0500 Subject: [PATCH 53/68] Improve shadow and highlighting in cell, column and row --- .../core/src/components/graphs/heatmap.ts | 272 ++++++++---------- packages/core/src/model/heatmap.ts | 26 +- packages/core/src/styles/graphs/_heatmap.scss | 51 ++-- 3 files changed, 155 insertions(+), 194 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index f1014e0c74..c1666ebcf5 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -13,6 +13,7 @@ import { DOMUtils } from '../../services'; import { get } from 'lodash-es'; // D3 Imports +import { min } from 'd3-array'; import { select } from 'd3-selection'; export class Heatmap extends Component { @@ -89,36 +90,37 @@ export class Heatmap extends Component { (yRange[1] - yRange[0]) / uniqueRange.length ); - // Add padding around chart so the axes lines display + // Determine padding depending on axes being used const axesOptions = Tools.getProperty(this.getOptions(), 'axes'); if (axesOptions) { Object.keys(axesOptions).forEach((axisPosition) => { switch (axisPosition) { case AxisPositions.TOP: - this.translationUnits[1] = + this.translationUnits.y = Configuration.heatmap.chartPadding; break; case AxisPositions.BOTTOM: - this.translationUnits[1] = -Configuration.heatmap + this.translationUnits.y = -Configuration.heatmap .chartPadding; break; case AxisPositions.LEFT: - this.translationUnits[0] = + this.translationUnits.x = Configuration.heatmap.chartPadding; break; case AxisPositions.RIGHT: - this.translationUnits[0] = -Configuration.heatmap + this.translationUnits.x = -Configuration.heatmap .chartPadding; break; } }); } + // Translate the chart to show the chart axes lines const container = svg .append('g') .attr( 'transform', - `translate(${this.translationUnits[0]}, ${this.translationUnits[1]})` + `translate(${this.translationUnits.x}, ${this.translationUnits.y})` ); const rectangles = container @@ -157,59 +159,68 @@ export class Heatmap extends Component { }) .attr('aria-label', (d) => d.value); - rectangles.exit().remove(); - - const rowAndColumnShadow = container - .append('g') - .classed('shadow-holder', true); - - // Row - rowAndColumnShadow - .append('rect') - .classed('highlighter-hidden', true) - .classed('shadow-row', true) - .attr('x', xRange[0]) - .attr('y', 0) - .attr('width', Math.abs(xRange[1] - xRange[0])) - .attr('height', this.yBandwidth); - - rowAndColumnShadow - .append('rect') - .classed('highlighter-hidden', true) - .classed('shadow-column', true) - .attr('x', xRange[0]) - .attr('y', 0) - .attr('width', this.xBandwidth) - .attr('height', Math.abs(yRange[1] - yRange[0])); - - const parent = DOMUtils.appendOrSelect( - this.parent, - 'g.row-column-highlighter' + // Cell highlight box + this.createOuterBox( + 'g.cell-highlight', + this.xBandwidth, + this.yBandwidth + ); + // Column highlight box + this.createOuterBox( + 'g.multi-cell.column-highlight', + this.xBandwidth, + Math.abs(yRange[1] - yRange[0]) + ); + // Row highlight box + this.createOuterBox( + 'g.multi-cell.row-highlight', + Math.abs(xRange[1] - xRange[0]), + this.yBandwidth ); - - DOMUtils.appendOrSelect(parent, 'rect.highlight-row') - .classed('highlighter-hidden', true) - .classed('highlight-row', true) - .attr('x', xRange[0]) - .attr('y', 0) - .attr('width', Math.abs(xRange[1] - xRange[0])) - .attr('height', this.yBandwidth); - - DOMUtils.appendOrSelect(parent, 'rect.highlight-column') - .classed('highlighter-hidden', true) - .attr('x', xRange[0]) - .attr('y', 0) - .attr('width', this.xBandwidth) - .attr('height', Math.abs(yRange[1] - yRange[0])); if (this.determineDividerStatus()) { rectangles.style('stroke-width', '1px'); + this.parent.select('g.cell-highlight').classed('cell-2', true); } this.addEventListener(); } - determineDividerStatus(): boolean { + /** + * Generates a box using lines to create a hover effect + * The lines have drop shadow in their respective direction + * @param parentTag - tag name + * @param xBandwidth - X length + * @param yBandwidth - y length + */ + private createOuterBox(parentTag, xBandwidth, yBandwidth) { + // Create a highlighter in the parent component so the shadow and the lines do not get clipped + const highlight = DOMUtils.appendOrSelect(this.parent, parentTag) + .classed('shadows', true) + .classed('highlighter-hidden', true); + + DOMUtils.appendOrSelect(highlight, 'line.top') + .attr('x1', 0) + .attr('x2', xBandwidth); + + DOMUtils.appendOrSelect(highlight, 'line.left') + .attr('x1', 0) + .attr('y1', yBandwidth); + + DOMUtils.appendOrSelect(highlight, 'line.down') + .attr('x1', 0) + .attr('x2', xBandwidth) + .attr('y1', yBandwidth) + .attr('y2', yBandwidth); + + DOMUtils.appendOrSelect(highlight, 'line.right') + .attr('x1', xBandwidth) + .attr('x2', xBandwidth) + .attr('y1', 0) + .attr('y2', yBandwidth); + } + + private determineDividerStatus(): boolean { // Add dividers if status is not off, will assume auto or on by default. const dividerStatus = Tools.getProperty( this.getOptions(), @@ -240,13 +251,6 @@ export class Heatmap extends Component { const { cartesianScales } = this.services; const options = this.getOptions(); const totalLabel = get(options, 'tooltip.totalLabel'); - // Add dividers if status is not off, will presume auto or on by default. - const dividerStatus = Tools.getProperty( - options, - 'heatmap', - 'divider', - 'state' - ); const domainIdentifier = cartesianScales.getDomainIdentifier(); const rangeIdentifier = cartesianScales.getRangeIdentifier(); @@ -263,7 +267,19 @@ export class Heatmap extends Component { // Dispatch event and tooltip only if value exists if (!nullState) { - const fillColor = hoveredElement.style('fill'); + // Get transformation value of node + const transform = Tools.getTranformOffsets( + cell.attr('transform') + ); + + select('g.cell-highlight') + .attr( + 'transform', + `translate(${ + transform.x + self.translationUnits.x + }, ${transform.y + self.translationUnits.y})` + ) + .classed('highlighter-hidden', false); // Dispatch mouse over event self.services.events.dispatchEvent( @@ -275,18 +291,6 @@ export class Heatmap extends Component { } ); - cell.raise(); - - // Highlight element - hoveredElement - .style( - 'stroke-width', - hoveredElement.style('stroke-width') === '1px' - ? '2px' - : '1px' - ) - .classed('raised', true); - // Dispatch tooltip show event self.services.events.dispatchEvent(Events.Tooltip.SHOW, { event, @@ -302,7 +306,7 @@ export class Heatmap extends Component { { label: totalLabel || 'Total', value: datum['value'], - color: fillColor, + color: hoveredElement.style('fill'), }, ], }); @@ -338,32 +342,27 @@ export class Heatmap extends Component { const cell = select(this); const hoveredElement = cell.select('rect.heat'); const nullState = hoveredElement.classed('null-state'); - hoveredElement.classed('raised', false); - cell.lower(); - - if (self.determineDividerStatus() && !nullState) { - hoveredElement.style('stroke-width', '1px'); - } else { - hoveredElement - .style('stroke', 'none') - .style('stroke-width', '0px'); - } - // Dispatch mouse out event - self.services.events.dispatchEvent( - Events.Heatmap.HEATMAP_MOUSEOUT, - { - event, - element: hoveredElement, - datum: datum, - } - ); + select('g.cell-highlight').classed('highlighter-hidden', true); - // Dispatch hide tooltip event - self.services.events.dispatchEvent(Events.Tooltip.HIDE, { - event, - hoveredElement, - }); + // Dispatch event and tooltip only if value exists + if (!nullState) { + // Dispatch mouse out event + self.services.events.dispatchEvent( + Events.Heatmap.HEATMAP_MOUSEOUT, + { + event, + element: hoveredElement, + datum: datum, + } + ); + + // Dispatch hide tooltip event + self.services.events.dispatchEvent(Events.Tooltip.HIDE, { + event, + hoveredElement, + }); + } }); } @@ -383,9 +382,8 @@ export class Heatmap extends Component { let label = '', sum = 0, - min = 0, - max = 0; - const ids = []; + minimum = 0, + maximum = 0; // Check to see where datum belongs if (this.matrix[datum] !== undefined) { @@ -394,61 +392,39 @@ export class Heatmap extends Component { ranges.forEach((element) => { let value = this.matrix[datum][element].value || 0; sum += value; - min = value < min ? value : min; - max = value > max ? value : max; - const id = this.matrix[datum][element].index; - if (id >= 0) { - ids.push(`g.heat-${id}`); - } + minimum = value < minimum ? value : minimum; + maximum = value > maximum ? value : maximum; }); } else { label = rangeLabel; domains.forEach((element) => { let value = this.matrix[element][datum].value || 0; sum += value; - min = value < min ? value : min; - max = value > max ? value : max; - const id = this.matrix[element][datum].index; - if (id >= 0) { - ids.push(`g.heat-${id}`); - } + minimum = value < minimum ? value : minimum; + maximum = value > maximum ? value : maximum; }); } - // Show the outlines to make the cells appear grouped - const shadowHighlighter = this.parent.select('g.shadow-holder').raise(); - // Pop out cells - this.parent - .selectAll(ids.join(',')) - .classed('axis-hovered', true) - .raise(); - if (mainXScale(datum) !== undefined) { - shadowHighlighter - .select('rect.shadow-column') - .classed('highlighter-hidden', false) - .attr('x', mainXScale(datum) + this.translationUnits[0]) - .raise(); - this.parent - .select('g.row-column-highlighter') - .select('rect.highlight-column') + .select('g.multi-cell.column-highlight') .classed('highlighter-hidden', false) - .attr('x', mainXScale(datum) + this.translationUnits[0]) - .raise(); + .attr( + 'transform', + `translate(${ + mainXScale(datum) + this.translationUnits.x + }, ${min(mainYScale.range()) + this.translationUnits.y})` + ); } else if (mainYScale(datum) !== undefined) { - shadowHighlighter - .select('rect.shadow-row') - .classed('highlighter-hidden', false) - .attr('y', mainXScale(datum) + this.translationUnits[1]) - .raise(); - this.parent - .select('g.row-column-highlighter') - .select('rect.highlight-row') + .select('g.multi-cell.row-highlight') .classed('highlighter-hidden', false) - .attr('y', mainXScale(datum) + this.translationUnits[1]) - .raise(); + .attr( + 'transform', + `translate(${ + min(mainXScale.range()) + this.translationUnits.x + },${mainYScale(datum) + this.translationUnits.y})` + ); } // Dispatch tooltip show event @@ -463,11 +439,11 @@ export class Heatmap extends Component { }, { label: 'Min', - value: min, + value: minimum, }, { label: 'Max', - value: max, + value: maximum, }, { label: 'Average', @@ -479,21 +455,9 @@ export class Heatmap extends Component { // Un-highlight all elements handleAxisMouseOut = (event: CustomEvent) => { - // Hide shadow - this.parent - .selectAll('rect.shadow-column,rect.shadow-row') - .classed('highlighter-hidden', true); - - // Lower cells + // Hide column/row this.parent - .selectAll('.axis-hovered') - .classed('axis-hovered', false) - .lower(); - - // Hide row/column highlighting - const strokeHighlighter = this.parent - .selectAll('g.row-column-highlighter') - .selectAll('rect.highlight-column, rect.highlight-row') + .selectAll('g.multi-cell') .classed('highlighter-hidden', true); // Dispatch hide tooltip event diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index a77a2bbbb5..2f591d1b9d 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -242,38 +242,16 @@ export class HeatmapModel extends ChartModelCartesian { let domainValueFormatter; const result = [ - [ - primaryDomain.label, - primaryRange.label, - ...(secondaryDomain ? [secondaryDomain.label] : []), - ...(secondaryRange ? [secondaryRange.label] : []), - 'Value', - ], + [primaryDomain.label, primaryRange.label, 'Value'], ...displayData.map((datum) => [ datum[primaryDomain.identifier] === null ? '–' : domainValueFormatter ? domainValueFormatter(datum[primaryDomain.identifier]) : datum[primaryDomain.identifier], - datum[primaryRange.identifier] === null || - isNaN(datum[primaryRange.identifier]) + datum[primaryRange.identifier] === null ? '–' : datum[primaryRange.identifier].toLocaleString(), - ...(secondaryDomain - ? [ - datum[secondaryDomain.identifier] === null - ? '–' - : datum[secondaryDomain.identifier], - ] - : []), - ...(secondaryRange - ? [ - datum[secondaryRange.identifier] === null || - isNaN(datum[secondaryRange.identifier]) - ? '–' - : datum[secondaryRange.identifier], - ] - : []), datum['value'], ]), ]; diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index b5df0fb59a..2e56f4d12f 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -1,26 +1,45 @@ .#{$prefix}--#{$charts-prefix}--heatmap { - rect.raised { - stroke: #ffffff !important; - filter: drop-shadow(0px 0px 3px black); + g.highlighter-hidden { + visibility: hidden; } - rect.highlighter-hidden { - visibility: hidden; + g.cell-highlight { + line { + stroke: white; + stroke-width: 1px; + } } - rect.shadow-column, - rect.shadow-row { - stroke: #ffffff; - fill: white; - stroke-width: 2px; - filter: drop-shadow(0px 0px 5px black); + g.cell-2 { + line { + stroke: white; + stroke-width: 2px !important; + } + } + + g.multi-cell { + line { + stroke: white; + stroke-width: 2px; + } } - rect.highlight-column, - rect.highlight-row { - stroke: #ffffff; - fill: none; - stroke-width: 2px; + g.shadows { + line.top { + filter: drop-shadow(0px -3px 2px black); + } + + line.down { + filter: drop-shadow(0px 3px 2px black); + } + + line.left { + filter: drop-shadow(-3px 0px 2px black); + } + + line.right { + filter: drop-shadow(3px 0px 2px black); + } } rect.null-state { From 9e04fd58b62d50a797e78ee39e4e6b4c41ae25c7 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Thu, 2 Dec 2021 00:35:57 -0500 Subject: [PATCH 54/68] Remove divider demo --- packages/core/demo/data/heatmap.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index cecb703d8e..00af391000 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -637,7 +637,7 @@ export const heatmapLegendOptions = { }; export const heatmapDomainOptions = { - title: 'Heatmap (Axis order option & no divider)', + title: 'Heatmap (Axis order option)', axes: { bottom: { title: 'Letters', @@ -664,11 +664,6 @@ export const heatmapDomainOptions = { ], }, }, - heatmap: { - divider: { - state: 'off', - }, - }, }; export const heatmapMissingData = [ From 89006fb66a2fdf0d50f5aea31d69aea288b3a780 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Fri, 3 Dec 2021 00:52:21 -0500 Subject: [PATCH 55/68] fix axis truncation hover --- packages/core/src/components/axes/axis.ts | 8 ++++++++ packages/core/src/components/axes/hover-axis.ts | 14 ++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/axes/axis.ts b/packages/core/src/components/axes/axis.ts index 0413c4f74d..dc89498f99 100644 --- a/packages/core/src/components/axes/axis.ts +++ b/packages/core/src/components/axes/axis.ts @@ -31,6 +31,12 @@ export class Axis extends Component { renderType = RenderTypes.SVG; margins: any; + truncation = { + [AxisPositions.LEFT]: false, + [AxisPositions.RIGHT]: false, + [AxisPositions.TOP]: false, + [AxisPositions.BOTTOM]: false, + }; scale: any; scaleType: ScaleTypes; @@ -645,11 +651,13 @@ export class Axis extends Component { container.selectAll('g.ticks g.tick').html(tick_html); + const self = this; container .selectAll('g.tick text') .data(axisTickLabels) .text(function (d) { if (d.length > truncationThreshold) { + self.truncation[axisPosition] = true; return Tools.truncateLabel( d, truncationType, diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index f79e0c31ed..04c271892c 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -24,6 +24,8 @@ export class HoverAxis extends Axis { container.selectAll('g.ticks').attr('tabindex', 0); + const self = this; + container.selectAll('g.tick').each(function () { const g = select(this); g.classed('tick-hover', true).attr('tabindex', -1); @@ -49,10 +51,20 @@ export class HoverAxis extends Axis { case AxisPositions.TOP: x = -(width / 2); y = -height + Number(textNode.attr('y')) / 2; + + if (self.truncation[axisPosition]) { + x = 0; + rectangle.attr('transform', `rotate(-45)`); + } break; case AxisPositions.BOTTOM: x = -(width / 2); y = height / 2 - 2; + + if (self.truncation[axisPosition]) { + x = -width; + rectangle.attr('transform', `rotate(-45)`); + } break; } @@ -65,8 +77,6 @@ export class HoverAxis extends Axis { rectangle.lower(); }); - const self = this; - container.selectAll('g.ticks').on('focus', function () { const axis = select(this); this.blur(); From a95cf8ff2becb1a440ab455b2b0ea2a1691d8589 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Sun, 5 Dec 2021 01:12:15 -0500 Subject: [PATCH 56/68] Fix bounding cell lines & highlighter issues --- .../core/src/components/axes/hover-axis.ts | 161 +++++------------- .../core/src/components/graphs/heatmap.ts | 67 ++------ 2 files changed, 56 insertions(+), 172 deletions(-) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 04c271892c..57303e0736 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -1,7 +1,6 @@ // Internal Imports import { Axis } from './axis'; -import { AxisPositions, Events, ScaleTypes } from '../../interfaces'; -import { Tools } from '../../tools'; +import { AxisPositions, Events } from '../../interfaces'; import { ChartModel } from '../../model/model'; import { DOMUtils } from '../../services'; @@ -22,13 +21,13 @@ export class HoverAxis extends Axis { `g.axis.${axisPosition}` ); - container.selectAll('g.ticks').attr('tabindex', 0); - const self = this; - - container.selectAll('g.tick').each(function () { + container.selectAll('g.tick').each(function (_, index) { const g = select(this); - g.classed('tick-hover', true).attr('tabindex', -1); + g.classed('tick-hover', true).attr( + 'tabindex', + index === 0 ? 0 : -1 + ); const textNode = g.select('text'); const { width, height } = DOMUtils.getSVGElementSize(textNode, { useBBox: true, @@ -75,46 +74,31 @@ export class HoverAxis extends Axis { .attr('height', height); rectangle.lower(); - }); - - container.selectAll('g.ticks').on('focus', function () { - const axis = select(this); - this.blur(); - - if (!axis.classed('invisible')) { - // Set focus on intial value in the axis - axis.select('g.tick').dispatch('focus'); - axis.selectAll('g.tick').on( - 'keydown', - function (event: KeyboardEvent) { - // Choose specific arrow key depending on the axis - if ( - axisPosition === AxisPositions.LEFT || - axisPosition === AxisPositions.RIGHT - ) { - if (event.key && event.key === 'ArrowUp') { - self.goNext(this as HTMLElement, event); - } else if (event.key && event.key === 'ArrowDown') { - self.goPrevious(this as HTMLElement, event); - } - } else { - if (event.key && event.key === 'ArrowLeft') { - self.goPrevious(this as HTMLElement, event); - } else if ( - event.key && - event.key === 'ArrowRight' - ) { - self.goNext(this as HTMLElement, event); - } - } + // Add keyboard event listeners to each group element + g.on('keydown', function (event: KeyboardEvent) { + // Choose specific arrow key depending on the axis + if ( + axisPosition === AxisPositions.LEFT || + axisPosition === AxisPositions.RIGHT + ) { + if (event.key && event.key === 'ArrowUp') { + self.goNext(this as HTMLElement, event); + } else if (event.key && event.key === 'ArrowDown') { + self.goPrevious(this as HTMLElement, event); } - ); - } + } else { + if (event.key && event.key === 'ArrowLeft') { + self.goPrevious(this as HTMLElement, event); + } else if (event.key && event.key === 'ArrowRight') { + self.goNext(this as HTMLElement, event); + } + } + }); }); // Add event listeners to elements drawn - this.addEventListeners(); + this.addFocusEventListeners(); } // Focus on the next HTML element sibling @@ -141,102 +125,37 @@ export class HoverAxis extends Axis { event.preventDefault(); } - addEventListeners() { + addFocusEventListeners() { const svg = this.getComponentContainer(); const { position: axisPosition } = this.configs; const container = DOMUtils.appendOrSelect( svg, `g.axis.${axisPosition}` ); - const options = this.getOptions(); - const axisOptions = Tools.getProperty(options, 'axes', axisPosition); - const axisScaleType = Tools.getProperty(axisOptions, 'scaleType'); - const truncationThreshold = Tools.getProperty( - axisOptions, - 'truncation', - 'threshold' - ); const self = this; - container - .selectAll('g.tick text') - .on('mouseover', function (event, datum) { - // Dispatch mouse event - self.services.events.dispatchEvent( - Events.Axis.LABEL_MOUSEOVER, - { - event, - element: select(this), - datum, - } - ); - - if ( - axisScaleType === ScaleTypes.LABELS && - datum.length > truncationThreshold - ) { - self.services.events.dispatchEvent(Events.Tooltip.SHOW, { - event, - hoveredElement: select(this), - content: datum, - }); - } - }) - .on('mousemove', function (event, datum) { - // Dispatch mouse event - self.services.events.dispatchEvent( - Events.Axis.LABEL_MOUSEMOVE, - { - event, - element: select(this), - datum, - } - ); - if ( - axisScaleType === ScaleTypes.LABELS && - datum.length > truncationThreshold - ) { - self.services.events.dispatchEvent(Events.Tooltip.MOVE, { - event, - }); - } - }) - .on('click', function (event, datum) { - // Dispatch mouse event - self.services.events.dispatchEvent(Events.Axis.LABEL_CLICK, { - event, - element: select(this), - datum, - }); - }) - .on('mouseout', function (event, datum) { - // Dispatch mouse event - self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, { - event, - element: select(this), - datum, - }); - - if (axisScaleType === ScaleTypes.LABELS) { - self.services.events.dispatchEvent(Events.Tooltip.HIDE); - } - }); - - // Emit mouseover & mouseout events on focus/blur container .selectAll('g.tick.tick-hover') .on('focus', function (event) { - // Focus element since we are using arrow keys - event.target.focus(); - // Dispatch mouse event + const coordinates = { clientX: 0, clientY: 0 }; + + if (event.target) { + // Focus element since we are using arrow keys + event.target.focus(); + const boundingRect = event.target.getBoundingClientRect(); + coordinates.clientX = boundingRect.x; + coordinates.clientY = boundingRect.y; + } + + // Dispatch focus event self.services.events.dispatchEvent(Events.Axis.LABEL_FOCUS, { - event, + event: { ...event, ...coordinates }, element: select(this), datum: select(this).select('text').datum(), }); }) .on('blur', function (event) { - // Dispatch mouse event + // Dispatch blur event self.services.events.dispatchEvent(Events.Axis.LABEL_BLUR, { event, element: select(this), diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index c1666ebcf5..4657ba9602 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -1,12 +1,7 @@ // Internal Imports import { Component } from '../component'; import * as Configuration from '../../configuration'; -import { - Events, - RenderTypes, - DividerStatus, - AxisPositions, -} from '../../interfaces'; +import { Events, RenderTypes, DividerStatus } from '../../interfaces'; import { Tools } from '../../tools'; import { DOMUtils } from '../../services'; @@ -90,38 +85,8 @@ export class Heatmap extends Component { (yRange[1] - yRange[0]) / uniqueRange.length ); - // Determine padding depending on axes being used - const axesOptions = Tools.getProperty(this.getOptions(), 'axes'); - if (axesOptions) { - Object.keys(axesOptions).forEach((axisPosition) => { - switch (axisPosition) { - case AxisPositions.TOP: - this.translationUnits.y = - Configuration.heatmap.chartPadding; - break; - case AxisPositions.BOTTOM: - this.translationUnits.y = -Configuration.heatmap - .chartPadding; - break; - case AxisPositions.LEFT: - this.translationUnits.x = - Configuration.heatmap.chartPadding; - break; - case AxisPositions.RIGHT: - this.translationUnits.x = -Configuration.heatmap - .chartPadding; - break; - } - }); - } - - // Translate the chart to show the chart axes lines - const container = svg - .append('g') - .attr( - 'transform', - `translate(${this.translationUnits.x}, ${this.translationUnits.y})` - ); + // Lower the chart so the axes are always visible + const container = svg.lower(); const rectangles = container .selectAll() @@ -200,24 +165,26 @@ export class Heatmap extends Component { .classed('highlighter-hidden', true); DOMUtils.appendOrSelect(highlight, 'line.top') - .attr('x1', 0) - .attr('x2', xBandwidth); + .attr('x1', -1) + .attr('x2', xBandwidth + 1); DOMUtils.appendOrSelect(highlight, 'line.left') .attr('x1', 0) - .attr('y1', yBandwidth); + .attr('y1', -1) + .attr('x2', 0) + .attr('y2', yBandwidth + 1); DOMUtils.appendOrSelect(highlight, 'line.down') - .attr('x1', 0) - .attr('x2', xBandwidth) + .attr('x1', -1) + .attr('x2', xBandwidth + 1) .attr('y1', yBandwidth) .attr('y2', yBandwidth); DOMUtils.appendOrSelect(highlight, 'line.right') .attr('x1', xBandwidth) .attr('x2', xBandwidth) - .attr('y1', 0) - .attr('y2', yBandwidth); + .attr('y1', -1) + .attr('y2', yBandwidth + 1); } private determineDividerStatus(): boolean { @@ -411,9 +378,9 @@ export class Heatmap extends Component { .classed('highlighter-hidden', false) .attr( 'transform', - `translate(${ - mainXScale(datum) + this.translationUnits.x - }, ${min(mainYScale.range()) + this.translationUnits.y})` + `translate(${mainXScale(datum)}, ${min( + mainYScale.range() + )})` ); } else if (mainYScale(datum) !== undefined) { this.parent @@ -421,9 +388,7 @@ export class Heatmap extends Component { .classed('highlighter-hidden', false) .attr( 'transform', - `translate(${ - min(mainXScale.range()) + this.translationUnits.x - },${mainYScale(datum) + this.translationUnits.y})` + `translate(${min(mainXScale.range())},${mainYScale(datum)})` ); } From 00ce1a9a111350a7f3ea0957dcc7aa34026b6d28 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 7 Dec 2021 21:32:24 -0500 Subject: [PATCH 57/68] Move heatmap demos to complex charts --- packages/core/demo/data/heatmap.ts | 13 +++++-- packages/core/demo/data/index.ts | 56 +++++++++++++++--------------- 2 files changed, 39 insertions(+), 30 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index 00af391000..d68a603d50 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -615,10 +615,13 @@ export const heatmapOptions = { scaleType: 'labels', }, }, + legend: { + colorLegend: { title: 'Legend title' }, + }, }; -export const heatmapLegendOptions = { - title: 'Heatmap (Quantize legend options)', +export const heatmapQuantizeLegendOption = { + title: 'Heatmap (Quantize legend)', axes: { bottom: { title: 'Letters', @@ -664,6 +667,9 @@ export const heatmapDomainOptions = { ], }, }, + legend: { + colorLegend: { title: 'Legend title' }, + }, }; export const heatmapMissingData = [ @@ -1123,4 +1129,7 @@ export const heatmapMissingDataOptions = { scaleType: 'labels', }, }, + legend: { + colorLegend: { title: 'Legend title' }, + }, }; diff --git a/packages/core/demo/data/index.ts b/packages/core/demo/data/index.ts index 5c53d54c50..6b2402e7ef 100644 --- a/packages/core/demo/data/index.ts +++ b/packages/core/demo/data/index.ts @@ -803,34 +803,6 @@ const simpleChartDemos = [ }, ], }, - { - title: 'Heatmap', - configs: { - excludeColorPaletteControl: true, - }, - demos: [ - { - options: heatmapDemos.heatmapOptions, - data: heatmapDemos.heatmapData, - chartType: chartTypes.HeatmapChart, - }, - { - options: heatmapDemos.heatmapLegendOptions, - data: heatmapDemos.heatmapData, - chartType: chartTypes.HeatmapChart, - }, - { - options: heatmapDemos.heatmapMissingDataOptions, - data: heatmapDemos.heatmapMissingData, - chartType: chartTypes.HeatmapChart, - }, - { - options: heatmapDemos.heatmapDomainOptions, - data: heatmapDemos.heatmapData, - chartType: chartTypes.HeatmapChart, - }, - ], - }, { title: 'Histogram', demos: [ @@ -1160,6 +1132,34 @@ const complexChartDemos = [ }, ], }, + { + title: 'Heatmap', + configs: { + excludeColorPaletteControl: true, + }, + demos: [ + { + options: heatmapDemos.heatmapOptions, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapQuantizeLegendOption, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapMissingDataOptions, + data: heatmapDemos.heatmapMissingData, + chartType: chartTypes.HeatmapChart, + }, + { + options: heatmapDemos.heatmapDomainOptions, + data: heatmapDemos.heatmapData, + chartType: chartTypes.HeatmapChart, + }, + ], + }, { title: 'Tree', configs: { From 734e6eb64c354fb92b3efa17236efa117f065fb9 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Tue, 7 Dec 2021 23:39:37 -0500 Subject: [PATCH 58/68] Add padding to hover rectangles & fix event dispatch issue --- packages/core/src/components/axes/axis.ts | 4 - .../core/src/components/axes/hover-axis.ts | 150 ++++++++++++++---- .../src/configuration-non-customizable.ts | 3 + 3 files changed, 118 insertions(+), 39 deletions(-) diff --git a/packages/core/src/components/axes/axis.ts b/packages/core/src/components/axes/axis.ts index dc89498f99..153016fa01 100644 --- a/packages/core/src/components/axes/axis.ts +++ b/packages/core/src/components/axes/axis.ts @@ -710,10 +710,6 @@ export class Axis extends Component { 'threshold' ); - const isTimeScaleType = - this.scaleType === ScaleTypes.TIME || - axisOptions.scaleType === ScaleTypes.TIME; - const self = this; container .selectAll('g.tick text') diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 57303e0736..76931ceab4 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -1,8 +1,10 @@ // Internal Imports import { Axis } from './axis'; -import { AxisPositions, Events } from '../../interfaces'; +import { AxisPositions, Events, ScaleTypes } from '../../interfaces'; import { ChartModel } from '../../model/model'; import { DOMUtils } from '../../services'; +import { Tools } from '../../tools'; +import * as Configuration from '../../configuration'; // D3 Imports import { select } from 'd3-selection'; @@ -14,6 +16,8 @@ export class HoverAxis extends Axis { render(animate = true) { super.render(animate); + // Remove existing event listeners to avoid flashing behavior + super.destroy(); const { position: axisPosition } = this.configs; const svg = this.getComponentContainer(); const container = DOMUtils.appendOrSelect( @@ -38,10 +42,12 @@ export class HoverAxis extends Axis { let x = 0, y = 0; + // Depending on axis position, apply correct translation & rotation to align the rect + // with the text switch (axisPosition) { case AxisPositions.LEFT: x = -width + Number(textNode.attr('x')); - y = -(height / 2) + 1; + y = -(height / 2); break; case AxisPositions.RIGHT: x = Math.abs(Number(textNode.attr('x'))); @@ -67,13 +73,17 @@ export class HoverAxis extends Axis { break; } + // Translates x position -4 left to keep center after padding + // Adds padding on left & right rectangle - .attr('x', x) + .attr('x', x - Configuration.axis.hover.rectanglePadding) .attr('y', y) - .attr('width', width) - .attr('height', height); - - rectangle.lower(); + .attr( + 'width', + width + Configuration.axis.hover.rectanglePadding * 2 + ) + .attr('height', height) + .lower(); // Add keyboard event listeners to each group element g.on('keydown', function (event: KeyboardEvent) { @@ -97,45 +107,91 @@ export class HoverAxis extends Axis { }); }); - // Add event listeners to elements drawn - this.addFocusEventListeners(); - } - - // Focus on the next HTML element sibling - private goNext(element: HTMLElement, event: Event) { - if ( - element.nextElementSibling !== null && - element.nextElementSibling.tagName !== 'path' - ) { - element.nextElementSibling.dispatchEvent(new Event('focus')); - } - - event.preventDefault(); + // Add event listeners to element group + this.addEventListeners(); } - // Focus on the previous HTML element sibling - private goPrevious(element: HTMLElement, event: Event) { - if ( - element.previousElementSibling !== null && - element.previousElementSibling.tagName !== 'path' - ) { - element.previousElementSibling.dispatchEvent(new Event('focus')); - } - - event.preventDefault(); - } - - addFocusEventListeners() { + addEventListeners() { const svg = this.getComponentContainer(); const { position: axisPosition } = this.configs; const container = DOMUtils.appendOrSelect( svg, `g.axis.${axisPosition}` ); + const options = this.getOptions(); + const axisOptions = Tools.getProperty(options, 'axes', axisPosition); + const axisScaleType = Tools.getProperty(axisOptions, 'scaleType'); + const truncationThreshold = Tools.getProperty( + axisOptions, + 'truncation', + 'threshold' + ); const self = this; container .selectAll('g.tick.tick-hover') + .on('mouseover', function (event) { + const hoveredElement = select(this).select('text'); + const datum = hoveredElement.datum() as string; + + // Dispatch mouse event + self.services.events.dispatchEvent( + Events.Axis.LABEL_MOUSEOVER, + { + event, + element: hoveredElement, + datum, + } + ); + + if ( + axisScaleType === ScaleTypes.LABELS && + datum.length > truncationThreshold + ) { + self.services.events.dispatchEvent(Events.Tooltip.SHOW, { + event, + element: hoveredElement, + datum, + }); + } + }) + .on('mousemove', function (event) { + const hoveredElement = select(this).select('text'); + const datum = hoveredElement.datum() as string; + // Dispatch mouse event + self.services.events.dispatchEvent( + Events.Axis.LABEL_MOUSEMOVE, + { + event, + element: hoveredElement, + datum, + } + ); + + self.services.events.dispatchEvent(Events.Tooltip.MOVE, { + event, + }); + }) + .on('click', function (event) { + // Dispatch mouse event + self.services.events.dispatchEvent(Events.Axis.LABEL_CLICK, { + event, + element: select(this).select('text'), + datum: select(this).select('text').datum(), + }); + }) + .on('mouseout', function (event) { + // Dispatch mouse event + self.services.events.dispatchEvent(Events.Axis.LABEL_MOUSEOUT, { + event, + element: select(this).select('text'), + datum: select(this).select('text').datum(), + }); + + if (axisScaleType === ScaleTypes.LABELS) { + self.services.events.dispatchEvent(Events.Tooltip.HIDE); + } + }) .on('focus', function (event) { const coordinates = { clientX: 0, clientY: 0 }; @@ -164,6 +220,30 @@ export class HoverAxis extends Axis { }); } + // Focus on the next HTML element sibling + private goNext(element: HTMLElement, event: Event) { + if ( + element.nextElementSibling && + element.nextElementSibling.tagName !== 'path' + ) { + element.nextElementSibling.dispatchEvent(new Event('focus')); + } + + event.preventDefault(); + } + + // Focus on the previous HTML element sibling + private goPrevious(element: HTMLElement, event: Event) { + if ( + element.previousElementSibling && + element.previousElementSibling.tagName !== 'path' + ) { + element.previousElementSibling.dispatchEvent(new Event('focus')); + } + + event.preventDefault(); + } + destroy() { const svg = this.getComponentContainer(); const { position: axisPosition } = this.configs; @@ -174,7 +254,7 @@ export class HoverAxis extends Axis { // Remove event listeners container - .selectAll('g.tick text') + .selectAll('g.tick.tick-hover') .on('mouseover', null) .on('mousemove', null) .on('mouseout', null) diff --git a/packages/core/src/configuration-non-customizable.ts b/packages/core/src/configuration-non-customizable.ts index 1fe056f082..800c398050 100644 --- a/packages/core/src/configuration-non-customizable.ts +++ b/packages/core/src/configuration-non-customizable.ts @@ -20,6 +20,9 @@ export const axis = { compareTo: 'marker', }, paddingRatio: 0.1, + hover: { + rectanglePadding: 4, + }, }; export const canvasZoomSettings = { From ee3c38aee17e87b48c9344b16fdde6a3faac062a Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 8 Dec 2021 03:19:03 -0500 Subject: [PATCH 59/68] Make color scale rect width responsive based on option width --- .../essentials/color-scale-legend.ts | 131 +++++++++++------- .../src/styles/components/_color-legend.scss | 2 +- 2 files changed, 79 insertions(+), 54 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 57cdb6d9d2..c45da7cb86 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -17,8 +17,6 @@ export class ColorScaleLegend extends Legend { private gradient_id = 'gradient-id-' + Math.floor(Math.random() * 99999999999); - private textWidth = 0; - init() { const eventsFragment = this.services.events; @@ -32,61 +30,67 @@ export class ColorScaleLegend extends Legend { handleAxisComplete = (event: CustomEvent) => { const svg = this.getComponentContainer(); - const title = Tools.getProperty( - this.getOptions(), - 'legend', - 'colorLegend', - 'title' - ); + const { width } = DOMUtils.getSVGElementSize(svg, { + useAttrs: true, + }); - const { cartesianScales } = this.services; - // Get available chart area - const mainXScale = cartesianScales.getMainXScale(); + if (width > Configuration.legend.color.barWidth) { + const title = Tools.getProperty( + this.getOptions(), + 'legend', + 'colorLegend', + 'title' + ); - const xDimensions = mainXScale.range(); + const { cartesianScales } = this.services; + // Get available chart area + const mainXScale = cartesianScales.getMainXScale(); - // Align legend with the axis - if (xDimensions[0] > 1) { - svg.select('g.legend-rectangle').attr( - 'transform', - `translate(${xDimensions[0]}, 0)` - ); + const xDimensions = mainXScale.range(); - if (title) { - const { - width: textWidth, - } = DOMUtils.getSVGElementSize( - svg.select('g.legend-title').select('text'), - { useBBox: true } + // Align legend with the axis + if (xDimensions[0] > 1) { + svg.select('g.legend-rectangle').attr( + 'transform', + `translate(${xDimensions[0]}, 0)` ); - // -9 since LEFT y-axis labels are moved towards the left by 9 - const availableSpace = xDimensions[0] - textWidth - 9; - - // If space is available align the the label with the axis labels - if (availableSpace > 1) { - svg.select('g.legend-title').attr( - 'transform', - `translate(${availableSpace}, 0)` - ); - } else { - // Move the legend down by 16 pixels to display legend text on top - svg.select('g.legend-rectangle').attr( - 'transform', - `translate(${xDimensions[0]}, 16)` + if (title) { + const { + width: textWidth, + } = DOMUtils.getSVGElementSize( + svg.select('g.legend-title').select('text'), + { useBBox: true } ); - // Align legend title with start of axis - svg.select('g.legend-title').attr( - 'transform', - `translate(${xDimensions[0]}, 0)` - ); + // -9 since LEFT y-axis labels are moved towards the left by 9 by d3 + const availableSpace = xDimensions[0] - textWidth - 9; + + // If space is available align the the label with the axis labels + if (availableSpace > 1) { + svg.select('g.legend-title').attr( + 'transform', + `translate(${availableSpace}, 0)` + ); + } else { + // Move the legend down by 16 pixels to display legend text on top + svg.select('g.legend-rectangle').attr( + 'transform', + `translate(${xDimensions[0]}, 16)` + ); + + // Align legend title with start of axis + svg.select('g.legend-title').attr( + 'transform', + `translate(${xDimensions[0]}, 0)` + ); + } } } } }; - render() { + render(animate = false) { const options = this.getOptions(); const customColors = Tools.getProperty( @@ -124,12 +128,27 @@ export class ColorScaleLegend extends Legend { svg.html('').attr('role', Roles.GROUP); const group = svg.append('g').classed('legend-rectangle', true); + const { width } = DOMUtils.getSVGElementSize(svg, { + useAttrs: true, + }); + + let barWidth = Configuration.legend.color.barWidth; + if (width <= Configuration.legend.color.barWidth) { + barWidth = width; + } + if (title) { svg.append('g') .classed('legend-title', true) .append('text') .text(title) .attr('dy', '0.7em'); + + // Move the legend down by 16 pixels to display legend text on top + svg.select('g.legend-rectangle').attr( + 'transform', + `translate(0, 16)` + ); } // If domain consists of negative and positive values, use diverging palettes @@ -186,14 +205,14 @@ export class ColorScaleLegend extends Legend { // Create the legend container const rectangle = group .append('rect') - .attr('width', Configuration.legend.color.barWidth) + .attr('width', barWidth) .attr('height', Configuration.legend.color.barHeight) .style('fill', `url(#${this.gradient_id}-legend)`); // Create scale & ticks const linearScale = scaleLinear() .domain(domain) - .range([0, Configuration.legend.color.barWidth]); + .range([0, barWidth]); domain.splice(1, 0, (domain[0] + domain[1]) / 2); const xAxis = axisBottom(linearScale) @@ -203,6 +222,7 @@ export class ColorScaleLegend extends Legend { // Align axes at the bottom of the rectangle and delete the domain line const axis = group .append('g') + .classed('legend-axis', true) .attr( 'transform', `translate(0,${Configuration.legend.color.axisYTranslation})` @@ -219,8 +239,6 @@ export class ColorScaleLegend extends Legend { const interpolator = interpolateRound(domain[0], domain[1]); const quant = quantize(interpolator, colorPairing.length); - // Remove white if divergent is used - const other = colorPairing; // If divergent && non-custom color, remove 0/white from being displayed if (!customColorsEnabled && colorScheme === 'diverge') { colorPairing.splice(colorPairing.length / 2, 1); @@ -228,7 +246,7 @@ export class ColorScaleLegend extends Legend { const colorScaleBand = scaleBand() .domain(colorPairing) - .rangeRound([0, Configuration.legend.color.barWidth]); + .rangeRound([0, barWidth]); const rectangle = group .selectAll('rect') @@ -275,10 +293,7 @@ export class ColorScaleLegend extends Legend { const firstTick = legendAxis.select('g.tick').clone(true); firstTick - .attr( - 'transform', - `translate(${Configuration.legend.color.barWidth}, 0)` - ) + .attr('transform', `translate(${barWidth}, 0)`) .classed('final-tick', true) .select('text') .text(quant[quant.length - 1]); @@ -289,6 +304,16 @@ export class ColorScaleLegend extends Legend { } else { throw Error('Entered color legend type is not supported.'); } + + // Translate last axis tick if barWidth equals chart width + if (width <= Configuration.legend.color.barWidth) { + const legend = svg.select('g.legend-axis'); + const lastTick = legend.select('g.tick:last-of-type text'); + const { width } = DOMUtils.getSVGElementSize(lastTick, { + useBBox: true, + }); + lastTick.attr('x', `-${width}`); + } } destroy() { diff --git a/packages/core/src/styles/components/_color-legend.scss b/packages/core/src/styles/components/_color-legend.scss index 4b952bdf34..b31336237c 100644 --- a/packages/core/src/styles/components/_color-legend.scss +++ b/packages/core/src/styles/components/_color-legend.scss @@ -1,7 +1,7 @@ svg.#{$prefix}--#{$charts-prefix}--color-legend { display: flex; user-select: none; - height: 38px; + height: inherit; @if $carbon--theme == $carbon--theme--g90 or From 64446547841402da27bf19e589a3fd0a522c2060 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 8 Dec 2021 03:19:48 -0500 Subject: [PATCH 60/68] Implement the requested pattern demo --- .../core/src/components/graphs/heatmap.ts | 27 ++++++++++++++----- packages/core/src/model/heatmap.ts | 8 +++--- packages/core/src/styles/graphs/_heatmap.scss | 4 +++ 3 files changed, 29 insertions(+), 10 deletions(-) diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index 4657ba9602..b82254b654 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -52,8 +52,9 @@ export class Heatmap extends Component { } render(animate = true) { - // svg and container widths - const svg = this.getComponentContainer(); + const svg = this.getComponentContainer({ withinChartClip: true }); + // Lower the chart so the axes are always visible + svg.lower(); const { cartesianScales } = this.services; this.matrix = this.model.getMatrix(); @@ -85,10 +86,24 @@ export class Heatmap extends Component { (yRange[1] - yRange[0]) / uniqueRange.length ); - // Lower the chart so the axes are always visible - const container = svg.lower(); + const patternID = this.services.domUtils.generateElementIDString( + `heatmap-pattern-stripes` + ); + + // Create a striped pattern for missing data + svg.append('defs') + .append('pattern') + .attr('id', patternID) + .attr('width', 3) + .attr('height', 3) + .attr('patternUnits', 'userSpaceOnUse') + .attr('patternTransform', 'rotate(45)') + .append('rect') + .classed('pattern-fill', true) + .attr('width', 0.5) + .attr('height', 8); - const rectangles = container + const rectangles = svg .selectAll() .data(matrixArray) .enter() @@ -118,7 +133,7 @@ export class Heatmap extends Component { .style('fill', (d) => { // Check if a valid value exists if (d.index === -1 || d.value === null) { - return null; + return `url(#${patternID})`; } return this.model.getFillColor(Number(d.value)); }) diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 2f591d1b9d..d9686381b1 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -52,19 +52,19 @@ export class HeatmapModel extends ChartModelCartesian { const limits = extent(data); const domain = []; - // Round extent values to the nearest multiple of 50 + // Round extent values to the nearest multiple of 10 // Axis rounds values to multiples of 2, 5, and 10s. limits.forEach((number, index) => { let value = Number(number); if (index === 0 && value >= 0) { value = 0; - } else if (value % 50 === 0 || value === 0) { + } else if (value % 10 === 0 || value === 0) { value; } else if (value < 0) { - value = Math.floor(value / 50) * 50; + value = Math.floor(value / 10) * 10; } else { - value = Math.ceil(value / 50) * 50; + value = Math.ceil(value / 10) * 10; } domain.push(value); diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 2e56f4d12f..84da962ee5 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -24,6 +24,10 @@ } } + rect.pattern-fill { + fill: $ui-04; + } + g.shadows { line.top { filter: drop-shadow(0px -3px 2px black); From b57e14cdf3db0d02fd454ad356aa40a1e1996213 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Wed, 8 Dec 2021 03:31:06 -0500 Subject: [PATCH 61/68] Add rendertype to color legend in heatmap --- packages/core/src/charts/heatmap.ts | 1 + packages/core/src/styles/components/_color-legend.scss | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index 740c3e1a58..536a45905b 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -96,6 +96,7 @@ export class HeatmapChart extends AxisChart { id: 'legend', components: [new ColorScaleLegend(this.model, this.services)], growth: LayoutGrowth.PREFERRED, + renderType: RenderTypes.SVG, }; const graphFrameComponent = { diff --git a/packages/core/src/styles/components/_color-legend.scss b/packages/core/src/styles/components/_color-legend.scss index b31336237c..5f9168dcb6 100644 --- a/packages/core/src/styles/components/_color-legend.scss +++ b/packages/core/src/styles/components/_color-legend.scss @@ -1,7 +1,6 @@ svg.#{$prefix}--#{$charts-prefix}--color-legend { display: flex; user-select: none; - height: inherit; @if $carbon--theme == $carbon--theme--g90 or From 4ba305b947a99820f6b5141508b83785e3f0a68b Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 20 Dec 2021 22:39:53 -0500 Subject: [PATCH 62/68] Fix requested changes --- packages/core/src/charts/heatmap.ts | 4 +- .../components/axes/two-dimensional-axes.ts | 19 +-- .../axes/two-dimensional-hover-axes.ts | 122 ------------------ .../essentials/color-scale-legend.ts | 104 ++++++++------- packages/core/src/components/index.ts | 1 - packages/core/src/interfaces/enums.ts | 10 +- packages/core/src/model/cartesian-charts.ts | 4 +- packages/core/src/model/heatmap.ts | 5 +- .../core/src/services/scales-cartesian.ts | 24 +++- packages/core/src/styles/graphs/_heatmap.scss | 36 +----- 10 files changed, 111 insertions(+), 218 deletions(-) delete mode 100644 packages/core/src/components/axes/two-dimensional-hover-axes.ts diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index 536a45905b..b03a7e4c64 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -15,7 +15,7 @@ import { import { Heatmap, - TwoDimensionalHoverAxes, + TwoDimensionalAxes, Modal, LayoutComponent, ColorScaleLegend, @@ -176,7 +176,7 @@ export class HeatmapChart extends AxisChart { getComponents() { // Specify what to render inside the graph-frame const graphFrameComponents = [ - new TwoDimensionalHoverAxes(this.model, this.services), + new TwoDimensionalAxes(this.model, this.services), new Heatmap(this.model, this.services), ]; diff --git a/packages/core/src/components/axes/two-dimensional-axes.ts b/packages/core/src/components/axes/two-dimensional-axes.ts index c42d9a9220..462b75bc0b 100644 --- a/packages/core/src/components/axes/two-dimensional-axes.ts +++ b/packages/core/src/components/axes/two-dimensional-axes.ts @@ -1,16 +1,12 @@ // Internal Imports import { Component } from '../component'; -import { - AxisPositions, - ScaleTypes, - AxesOptions, - RenderTypes, -} from '../../interfaces'; +import { AxisPositions, RenderTypes, AxisFlavor } from '../../interfaces'; import { Axis } from './axis'; import { Tools } from '../../tools'; import { DOMUtils } from '../../services'; import { Threshold } from '../essentials/threshold'; import { Events } from './../../interfaces'; +import { HoverAxis } from './hover-axis'; export class TwoDimensionalAxes extends Component { type = '2D-axes'; @@ -48,11 +44,16 @@ export class TwoDimensionalAxes extends Component { this.configs.axes[axisPosition] && !this.children[axisPosition] ) { - const axisComponent = new Axis(this.model, this.services, { + const configs = { position: axisPosition, axes: this.configs.axes, margins: this.margins, - }); + }; + + const axisComponent = + this.model.axisFlavor === AxisFlavor.DEFAULT + ? new Axis(this.model, this.services, configs) + : new HoverAxis(this.model, this.services, configs); // Set model, services & parent for the new axis component axisComponent.setModel(this.model); @@ -117,6 +118,8 @@ export class TwoDimensionalAxes extends Component { } }); + this.services.events.dispatchEvent(Events.Axis.RENDER_COMPLETE); + // If the new margins are different than the existing ones const isNotEqual = Object.keys(margins).some((marginKey) => { return this.margins[marginKey] !== margins[marginKey]; diff --git a/packages/core/src/components/axes/two-dimensional-hover-axes.ts b/packages/core/src/components/axes/two-dimensional-hover-axes.ts deleted file mode 100644 index 90b4ee19c4..0000000000 --- a/packages/core/src/components/axes/two-dimensional-hover-axes.ts +++ /dev/null @@ -1,122 +0,0 @@ -// Internal Imports -import { TwoDimensionalAxes } from './two-dimensional-axes'; -import { AxisPositions } from '../../interfaces'; -import { Tools } from '../../tools'; -import { DOMUtils } from '../../services'; -import { Events } from './../../interfaces'; -import { HoverAxis } from './hover-axis'; - -export class TwoDimensionalHoverAxes extends TwoDimensionalAxes { - render(animate = false) { - const axes = {}; - const axisPositions = Object.keys(AxisPositions); - const axesOptions = Tools.getProperty(this.getOptions(), 'axes'); - - axisPositions.forEach((axisPosition) => { - const axisOptions = axesOptions[AxisPositions[axisPosition]]; - if (axisOptions) { - axes[AxisPositions[axisPosition]] = true; - } - }); - - this.configs.axes = axes; - - // Check the configs to know which axes need to be rendered - axisPositions.forEach((axisPositionKey) => { - const axisPosition = AxisPositions[axisPositionKey]; - if ( - this.configs.axes[axisPosition] && - !this.children[axisPosition] - ) { - const axisComponent = new HoverAxis(this.model, this.services, { - position: axisPosition, - axes: this.configs.axes, - margins: this.margins, - }); - - // Set model, services & parent for the new axis component - axisComponent.setModel(this.model); - axisComponent.setServices(this.services); - axisComponent.setParent(this.parent); - - this.children[axisPosition] = axisComponent; - } - }); - - Object.keys(this.children).forEach((childKey) => { - const child = this.children[childKey]; - child.render(animate); - }); - - const margins = {} as any; - - Object.keys(this.children).forEach((childKey) => { - const child = this.children[childKey]; - const axisPosition = child.configs.position; - - // Grab the invisibly rendered axis' width & height, and set margins - // Based off of that - // We draw the invisible axis because of the async nature of d3 transitions - // To be able to tell the final width & height of the axis when initiaing the transition - // The invisible axis is updated instantly and without a transition - const invisibleAxisRef = child.getInvisibleAxisRef(); - const { - width, - height, - } = DOMUtils.getSVGElementSize(invisibleAxisRef, { useBBox: true }); - - let offset; - if (child.getTitleRef().empty()) { - offset = 0; - } else { - offset = DOMUtils.getSVGElementSize(child.getTitleRef(), { - useBBox: true, - }).height; - - if ( - axisPosition === AxisPositions.LEFT || - axisPosition === AxisPositions.RIGHT - ) { - offset += 5; - } - } - - switch (axisPosition) { - case AxisPositions.TOP: - margins.top = height + offset; - break; - case AxisPositions.BOTTOM: - margins.bottom = height + offset; - break; - case AxisPositions.LEFT: - margins.left = width + offset; - break; - case AxisPositions.RIGHT: - margins.right = width + offset; - break; - } - }); - - this.services.events.dispatchEvent(Events.Axis.RENDER_COMPLETE); - - // If the new margins are different than the existing ones - const isNotEqual = Object.keys(margins).some((marginKey) => { - return this.margins[marginKey] !== margins[marginKey]; - }); - - if (isNotEqual) { - this.margins = Object.assign(this.margins, margins); - - // also set new margins to model to allow external components to access - this.model.set({ axesMargins: this.margins }, { skipUpdate: true }); - this.services.events.dispatchEvent(Events.ZoomBar.UPDATE); - - Object.keys(this.children).forEach((childKey) => { - const child = this.children[childKey]; - child.margins = this.margins; - }); - - this.render(true); - } - } -} diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index c45da7cb86..3587495f37 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -50,7 +50,7 @@ export class ColorScaleLegend extends Legend { // Align legend with the axis if (xDimensions[0] > 1) { - svg.select('g.legend-rectangle').attr( + svg.select('g.legend').attr( 'transform', `translate(${xDimensions[0]}, 0)` ); @@ -74,7 +74,7 @@ export class ColorScaleLegend extends Legend { ); } else { // Move the legend down by 16 pixels to display legend text on top - svg.select('g.legend-rectangle').attr( + svg.select('g.legend').attr( 'transform', `translate(${xDimensions[0]}, 16)` ); @@ -125,8 +125,8 @@ export class ColorScaleLegend extends Legend { const domain = this.model.getValueDomain(); const svg = this.getComponentContainer(); - svg.html('').attr('role', Roles.GROUP); - const group = svg.append('g').classed('legend-rectangle', true); + const legend = DOMUtils.appendOrSelect(svg, 'g.legend'); + const axis = DOMUtils.appendOrSelect(legend, 'g.legend-axis'); const { width } = DOMUtils.getSVGElementSize(svg, { useAttrs: true, @@ -138,17 +138,18 @@ export class ColorScaleLegend extends Legend { } if (title) { - svg.append('g') - .classed('legend-title', true) - .append('text') - .text(title) - .attr('dy', '0.7em'); + const legendTitleGroup = DOMUtils.appendOrSelect( + svg, + 'g.legend-title' + ); + const legendTitle = DOMUtils.appendOrSelect( + legendTitleGroup, + 'text' + ); + legendTitle.text(title).attr('dy', '0.7em'); // Move the legend down by 16 pixels to display legend text on top - svg.select('g.legend-rectangle').attr( - 'transform', - `translate(0, 16)` - ); + legend.attr('transform', `translate(0, 16)`); } // If domain consists of negative and positive values, use diverging palettes @@ -191,8 +192,11 @@ export class ColorScaleLegend extends Legend { const stopLengthPercentage = 100 / (colorPairing.length - 1); // Generate the gradient - const linearGradient = group - .append('linearGradient') + const linearGradient = DOMUtils.appendOrSelect( + legend, + 'linearGradient' + ); + linearGradient .attr('id', `${this.gradient_id}-legend`) .selectAll('stop') .data(colorPairing) @@ -203,8 +207,8 @@ export class ColorScaleLegend extends Legend { .attr('stop-color', (d) => d); // Create the legend container - const rectangle = group - .append('rect') + const rectangle = DOMUtils.appendOrSelect(legend, 'rect'); + rectangle .attr('width', barWidth) .attr('height', Configuration.legend.color.barHeight) .style('fill', `url(#${this.gradient_id}-legend)`); @@ -220,14 +224,10 @@ export class ColorScaleLegend extends Legend { .tickValues(domain); // Align axes at the bottom of the rectangle and delete the domain line - const axis = group - .append('g') - .classed('legend-axis', true) - .attr( - 'transform', - `translate(0,${Configuration.legend.color.axisYTranslation})` - ) - .call(xAxis); + axis.attr( + 'transform', + `translate(0,${Configuration.legend.color.axisYTranslation})` + ).call(xAxis); // Remove domain axis.select('.domain').remove(); @@ -246,15 +246,21 @@ export class ColorScaleLegend extends Legend { const colorScaleBand = scaleBand() .domain(colorPairing) - .rangeRound([0, barWidth]); + .range([0, barWidth]); + + // Render the quantized rectangles + const rectangle = DOMUtils.appendOrSelect( + legend, + 'g.quantized-rect' + ); - const rectangle = group + rectangle .selectAll('rect') .data(colorScaleBand.domain()) .join('rect') - .attr('x', colorScaleBand) + .attr('x', (d) => colorScaleBand(d)) .attr('y', 0) - .attr('width', Math.max(0, colorScaleBand.bandwidth() - 1)) + .attr('width', Math.max(0, colorScaleBand.bandwidth()) - 1) .attr('height', Configuration.legend.color.barHeight) .attr('class', (d) => d) .attr('fill', (d) => d); @@ -276,39 +282,41 @@ export class ColorScaleLegend extends Legend { }); // Align axis to match bandwidth start after initial (white) - const legendAxis = group - .append('g') - .classed('legend-axis', true) + const axisTranslation = colorScaleBand.bandwidth() / 2; + axis.attr( + 'transform', + `translate(${ + !customColorsEnabled && colorScheme === 'diverge' ? '-' : '' + }${axisTranslation}, ${ + Configuration.legend.color.axisYTranslation + })` + ).call(xAxis); + + // Append the last tick + const firstTick = axis.select('g.tick').clone(true); + firstTick .attr( 'transform', `translate(${ - !customColorsEnabled && colorScheme === 'diverge' - ? '-' - : '' - }${colorScaleBand.bandwidth() / 2}, ${ - Configuration.legend.color.axisYTranslation - })` + barWidth + + (!customColorsEnabled && colorScheme === 'diverge' + ? axisTranslation + : -axisTranslation) + }, 0)` ) - .call(xAxis); - - const firstTick = legendAxis.select('g.tick').clone(true); - firstTick - .attr('transform', `translate(${barWidth}, 0)`) .classed('final-tick', true) .select('text') .text(quant[quant.length - 1]); - legendAxis.enter().append(firstTick.node()).raise(); - - legendAxis.select('.domain').remove(); + axis.enter().append(firstTick.node()); + axis.select('.domain').remove(); } else { throw Error('Entered color legend type is not supported.'); } // Translate last axis tick if barWidth equals chart width if (width <= Configuration.legend.color.barWidth) { - const legend = svg.select('g.legend-axis'); - const lastTick = legend.select('g.tick:last-of-type text'); + const lastTick = axis.select('g.tick:last-of-type text'); const { width } = DOMUtils.getSVGElementSize(lastTick, { useBBox: true, }); diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 491b32c208..c7e1b238f3 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -45,7 +45,6 @@ export * from './layout/layout'; // MISC export * from './axes/two-dimensional-axes'; -export * from './axes/two-dimensional-hover-axes'; export * from './axes/axis'; export * from './axes/grid-brush'; export * from './axes/chart-clip'; diff --git a/packages/core/src/interfaces/enums.ts b/packages/core/src/interfaces/enums.ts index de83ea322b..2180abf2e9 100644 --- a/packages/core/src/interfaces/enums.ts +++ b/packages/core/src/interfaces/enums.ts @@ -260,10 +260,18 @@ export enum ColorLegendType { } /** - * enum of axis ticks rotation + * enum of divider status for heatmap */ export enum DividerStatus { ON = 'on', AUTO = 'auto', OFF = 'off', } + +/** + * enum of axis flavor + */ +export enum AxisFlavor { + DEFAULT = 'default', + HOVER = 'hover', +} diff --git a/packages/core/src/model/cartesian-charts.ts b/packages/core/src/model/cartesian-charts.ts index f24f6eafab..26103f7e81 100644 --- a/packages/core/src/model/cartesian-charts.ts +++ b/packages/core/src/model/cartesian-charts.ts @@ -1,7 +1,7 @@ // Internal Imports import { ChartModel } from './model'; import { Tools } from '../tools'; -import { ScaleTypes, AxisPositions } from '../interfaces'; +import { ScaleTypes, AxisPositions, AxisFlavor } from '../interfaces'; // date formatting import { format } from 'date-fns'; @@ -10,6 +10,8 @@ import { format } from 'date-fns'; * This supports adding X and Y Cartesian[2D] zoom data to a ChartModel * */ export class ChartModelCartesian extends ChartModel { + protected axisFlavour = AxisFlavor.DEFAULT; + constructor(services: any) { super(services); } diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index d9686381b1..d33563b7dc 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -1,5 +1,5 @@ // Internal Imports -import { ScaleTypes } from '../interfaces'; +import { AxisFlavor, ScaleTypes } from '../interfaces'; import { ChartModelCartesian } from './cartesian-charts'; import { Tools } from '../tools'; @@ -9,7 +9,7 @@ import { scaleQuantize } from 'd3-scale'; /** The gauge chart model layer */ export class HeatmapModel extends ChartModelCartesian { - selectedPalette = []; + protected axisFlavour = AxisFlavor.HOVER; private _colorScale: any = undefined; // List of unique ranges and domains @@ -318,7 +318,6 @@ export class HeatmapModel extends ChartModelCartesian { } // Save scale type - this.selectedPalette = colorPairing; this._colorScale = scaleQuantize() .domain(this.getValueDomain() as [number, number]) .range(colorPairing); diff --git a/packages/core/src/services/scales-cartesian.ts b/packages/core/src/services/scales-cartesian.ts index aea8b19461..256a6f9d2f 100644 --- a/packages/core/src/services/scales-cartesian.ts +++ b/packages/core/src/services/scales-cartesian.ts @@ -215,12 +215,34 @@ export class CartesianScales extends Service { } getCustomDomainValuesByposition(axisPosition: AxisPositions) { - return Tools.getProperty( + const domain = Tools.getProperty( this.model.getOptions(), 'axes', axisPosition, 'domain' ); + + // Check if domain is an array + if (domain && !Array.isArray(domain)) { + throw new Error( + `Domain in ${axisPosition} axis is not a valid array` + ); + } + + // Determine number of elements passed in domain depending on scale types + if (Array.isArray(domain)) { + if ( + (this.scaleTypes[axisPosition] === ScaleTypes.LINEAR || + this.scaleTypes[axisPosition] === ScaleTypes.TIME) && + domain.length !== 2 + ) { + throw new Error( + `There can only be 2 elements in domain for scale type: ${this.scaleTypes[axisPosition]}` + ); + } + } + + return domain; } getOrientation() { diff --git a/packages/core/src/styles/graphs/_heatmap.scss b/packages/core/src/styles/graphs/_heatmap.scss index 84da962ee5..50512e6190 100644 --- a/packages/core/src/styles/graphs/_heatmap.scss +++ b/packages/core/src/styles/graphs/_heatmap.scss @@ -54,43 +54,17 @@ stroke-width: 0px; } - @if $carbon--theme == $carbon--theme--white { - rect.heat { - stroke: #ffffff; - } - - rect.null-state { - fill: #f4f4f4; - } + rect.heat { + stroke: $ui-background; } - @if $carbon--theme == $carbon--theme--g10 { - rect.heat { - stroke: #f4f4f4; - } - - rect.null-state { - fill: #ffffff; - } + rect.null-state { + fill: $ui-01; } @if $carbon--theme == $carbon--theme--g90 { - rect.heat { - stroke: #262626; - } - - rect.null-state { - fill: #161616; - } - } - - @if $carbon--theme == $carbon--theme--g100 { - rect.heat { - stroke: #161616; - } - rect.null-state { - fill: #262626; + fill: $inverse-01; } } } From d846b129e706a8c31f94cfcaa6869309a41f11d6 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Mon, 20 Dec 2021 22:46:07 -0500 Subject: [PATCH 63/68] Correct model variable spelling --- packages/core/src/model/cartesian-charts.ts | 2 +- packages/core/src/model/heatmap.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/model/cartesian-charts.ts b/packages/core/src/model/cartesian-charts.ts index 26103f7e81..873b1b310c 100644 --- a/packages/core/src/model/cartesian-charts.ts +++ b/packages/core/src/model/cartesian-charts.ts @@ -10,7 +10,7 @@ import { format } from 'date-fns'; * This supports adding X and Y Cartesian[2D] zoom data to a ChartModel * */ export class ChartModelCartesian extends ChartModel { - protected axisFlavour = AxisFlavor.DEFAULT; + protected axisFlavor = AxisFlavor.DEFAULT; constructor(services: any) { super(services); diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index d33563b7dc..1313d28641 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -9,7 +9,7 @@ import { scaleQuantize } from 'd3-scale'; /** The gauge chart model layer */ export class HeatmapModel extends ChartModelCartesian { - protected axisFlavour = AxisFlavor.HOVER; + protected axisFlavor = AxisFlavor.HOVER; private _colorScale: any = undefined; // List of unique ranges and domains From a74cbf4608fadc1777d7938a0c971e9d279d41c3 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Thu, 23 Dec 2021 00:12:02 -0500 Subject: [PATCH 64/68] Performance improvements & enum updates --- packages/core/src/charts/heatmap.ts | 7 ++- .../essentials/color-scale-legend.ts | 21 ++++++++- .../core/src/components/graphs/heatmap.ts | 4 ++ packages/core/src/interfaces/enums.ts | 2 +- packages/core/src/model/heatmap.ts | 45 ++++++++++++++----- 5 files changed, 64 insertions(+), 15 deletions(-) diff --git a/packages/core/src/charts/heatmap.ts b/packages/core/src/charts/heatmap.ts index b03a7e4c64..d3fe009b40 100644 --- a/packages/core/src/charts/heatmap.ts +++ b/packages/core/src/charts/heatmap.ts @@ -51,7 +51,10 @@ export class HeatmapChart extends AxisChart { // Custom getChartComponents - Implements getChartComponents // Removes zoombar support and additional `features` that are not supported in heatmap - private getChartComponentsList(graphFrameComponents: any[], configs?: any) { + protected getAxisChartComponents( + graphFrameComponents: any[], + configs?: any + ) { const options = this.model.getOptions(); const toolbarEnabled = Tools.getProperty(options, 'toolbar', 'enabled'); @@ -180,7 +183,7 @@ export class HeatmapChart extends AxisChart { new Heatmap(this.model, this.services), ]; - const components: any[] = this.getChartComponentsList( + const components: any[] = this.getAxisChartComponents( graphFrameComponents ); return components; diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 3587495f37..281c5187e5 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -34,7 +34,13 @@ export class ColorScaleLegend extends Legend { useAttrs: true, }); - if (width > Configuration.legend.color.barWidth) { + const isDataLoading = Tools.getProperty( + this.getOptions(), + 'data', + 'loading' + ); + + if (width > Configuration.legend.color.barWidth && !isDataLoading) { const title = Tools.getProperty( this.getOptions(), 'legend', @@ -125,6 +131,19 @@ export class ColorScaleLegend extends Legend { const domain = this.model.getValueDomain(); const svg = this.getComponentContainer(); + + // Clear DOM if loading + const isDataLoading = Tools.getProperty( + this.getOptions(), + 'data', + 'loading' + ); + + if (isDataLoading) { + svg.html(''); + return; + } + const legend = DOMUtils.appendOrSelect(svg, 'g.legend'); const axis = DOMUtils.appendOrSelect(legend, 'g.legend-axis'); diff --git a/packages/core/src/components/graphs/heatmap.ts b/packages/core/src/components/graphs/heatmap.ts index b82254b654..da0b233e77 100644 --- a/packages/core/src/components/graphs/heatmap.ts +++ b/packages/core/src/components/graphs/heatmap.ts @@ -61,6 +61,10 @@ export class Heatmap extends Component { svg.html(''); + if (Tools.getProperty(this.getOptions(), 'data', 'loading')) { + return; + } + // determine x and y axis scale const mainXScale = cartesianScales.getMainXScale(); const mainYScale = cartesianScales.getMainYScale(); diff --git a/packages/core/src/interfaces/enums.ts b/packages/core/src/interfaces/enums.ts index 2180abf2e9..9b9e85ac74 100644 --- a/packages/core/src/interfaces/enums.ts +++ b/packages/core/src/interfaces/enums.ts @@ -273,5 +273,5 @@ export enum DividerStatus { */ export enum AxisFlavor { DEFAULT = 'default', - HOVER = 'hover', + HOVERABLE = 'hoverable', } diff --git a/packages/core/src/model/heatmap.ts b/packages/core/src/model/heatmap.ts index 1313d28641..1ed7030df9 100644 --- a/packages/core/src/model/heatmap.ts +++ b/packages/core/src/model/heatmap.ts @@ -9,7 +9,7 @@ import { scaleQuantize } from 'd3-scale'; /** The gauge chart model layer */ export class HeatmapModel extends ChartModelCartesian { - protected axisFlavor = AxisFlavor.HOVER; + protected axisFlavor = AxisFlavor.HOVERABLE; private _colorScale: any = undefined; // List of unique ranges and domains @@ -169,17 +169,19 @@ export class HeatmapModel extends ChartModelCartesian { const domainIdentifier = this.services.cartesianScales.getDomainIdentifier(); const rangeIdentifier = this.services.cartesianScales.getRangeIdentifier(); - // Create matrix (domain by range) and initalize it's values to null + // Create a column + const range = {}; + uniqueRange.forEach((ran: any) => { + // Initialize matrix to empty state + range[ran] = { + value: null, + index: -1, + }; + }); + + // Complete the matrix by cloning the column to all domains uniqueDomain.forEach((dom: any) => { - const range = {}; - // Data will be set to null by default, to signify 'missing' - uniqueRange.forEach((element: any) => { - range[element] = { - value: null, - index: -1, - }; - }); - this._matrix[dom] = range; + this._matrix[dom] = Tools.clone(range); }); // Fill in user passed data @@ -194,6 +196,27 @@ export class HeatmapModel extends ChartModelCartesian { return this._matrix; } + /** + * + * @param newData The new raw data to be set + */ + setData(newData) { + const sanitizedData = this.sanitize(Tools.clone(newData)); + const dataGroups = this.generateDataGroups(sanitizedData); + + this.set({ + data: sanitizedData, + dataGroups, + }); + + // Set attributes to empty + this._domains = []; + this._ranges = []; + this._matrix = {}; + + return sanitizedData; + } + /** * Converts Object matrix into a single array * @returns Object[] From 07d663654ef2f70c2133f6900507ddfc5e14eeb6 Mon Sep 17 00:00:00 2001 From: Eliad Moosavi Date: Thu, 23 Dec 2021 11:57:28 -0500 Subject: [PATCH 65/68] Update packages/core/src/components/axes/hover-axis.ts --- packages/core/src/components/axes/hover-axis.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 76931ceab4..9e657dee63 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -16,6 +16,7 @@ export class HoverAxis extends Axis { render(animate = true) { super.render(animate); + // Remove existing event listeners to avoid flashing behavior super.destroy(); const { position: axisPosition } = this.configs; From f6c8a45cf5079d23a3b0ad729eb57bd1e59ba984 Mon Sep 17 00:00:00 2001 From: Eliad Moosavi Date: Thu, 23 Dec 2021 11:57:50 -0500 Subject: [PATCH 66/68] Update packages/core/src/components/axes/hover-axis.ts --- packages/core/src/components/axes/hover-axis.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/core/src/components/axes/hover-axis.ts b/packages/core/src/components/axes/hover-axis.ts index 9e657dee63..93c2a4f8db 100644 --- a/packages/core/src/components/axes/hover-axis.ts +++ b/packages/core/src/components/axes/hover-axis.ts @@ -19,6 +19,7 @@ export class HoverAxis extends Axis { // Remove existing event listeners to avoid flashing behavior super.destroy(); + const { position: axisPosition } = this.configs; const svg = this.getComponentContainer(); const container = DOMUtils.appendOrSelect( From 8fb5e82f72963bccd2d95f5f130cc3cbd90e669d Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Thu, 23 Dec 2021 23:33:32 -0500 Subject: [PATCH 67/68] Remove color legend options from legend options --- .../src/components/essentials/color-scale-legend.ts | 6 +++--- packages/core/src/configuration.ts | 2 -- packages/core/src/interfaces/charts.ts | 13 +++++++++++++ packages/core/src/interfaces/components.ts | 12 ------------ 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/packages/core/src/components/essentials/color-scale-legend.ts b/packages/core/src/components/essentials/color-scale-legend.ts index 281c5187e5..c0d9c8a4ca 100644 --- a/packages/core/src/components/essentials/color-scale-legend.ts +++ b/packages/core/src/components/essentials/color-scale-legend.ts @@ -43,7 +43,7 @@ export class ColorScaleLegend extends Legend { if (width > Configuration.legend.color.barWidth && !isDataLoading) { const title = Tools.getProperty( this.getOptions(), - 'legend', + 'heatmap', 'colorLegend', 'title' ); @@ -108,7 +108,7 @@ export class ColorScaleLegend extends Legend { const colorScaleType = Tools.getProperty( options, - 'legend', + 'heatmap', 'colorLegend', 'type' ); @@ -122,7 +122,7 @@ export class ColorScaleLegend extends Legend { const title = Tools.getProperty( options, - 'legend', + 'heatmap', 'colorLegend', 'title' ); diff --git a/packages/core/src/configuration.ts b/packages/core/src/configuration.ts index 1c80f4554a..8eaf31d102 100644 --- a/packages/core/src/configuration.ts +++ b/packages/core/src/configuration.ts @@ -598,8 +598,6 @@ const heatmapChart: HeatmapChartOptions = Tools.merge({}, chart, { divider: { state: DividerStatus.AUTO, }, - }, - legend: { colorLegend: { type: 'linear', }, diff --git a/packages/core/src/interfaces/charts.ts b/packages/core/src/interfaces/charts.ts index 03997e6937..d2028926b5 100644 --- a/packages/core/src/interfaces/charts.ts +++ b/packages/core/src/interfaces/charts.ts @@ -6,6 +6,7 @@ import { ChartTypes, TreeTypes, DividerStatus, + ColorLegendType, } from './enums'; import { LegendOptions, @@ -531,5 +532,17 @@ export interface HeatmapChartOptions extends BaseChartOptions { divider?: { state?: DividerStatus; }; + /** + * customize color legend + * enabled by default on select charts + */ + colorLegend?: { + /** + * Text to display beside or on top of the legend + * Position is determined by text length + */ + title?: string; + type: ColorLegendType; + }; }; } diff --git a/packages/core/src/interfaces/components.ts b/packages/core/src/interfaces/components.ts index b930449b89..d8a303b1b0 100644 --- a/packages/core/src/interfaces/components.ts +++ b/packages/core/src/interfaces/components.ts @@ -45,18 +45,6 @@ export interface LegendOptions { * customized legend items */ additionalItems?: LegendItem[]; - /** - * customize color legend - * enabled by default on select charts - */ - colorLegend?: { - /** - * Text to display beside or on top of the legend - * Position is determined by text length - */ - title?: string; - type: ColorLegendType; - }; } /** From 1d857972f6a68a76ec2d71512e8efa3fcbf01655 Mon Sep 17 00:00:00 2001 From: Akshat Patel Date: Thu, 23 Dec 2021 23:34:03 -0500 Subject: [PATCH 68/68] Make heatmap experimental --- packages/core/demo/data/heatmap.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/core/demo/data/heatmap.ts b/packages/core/demo/data/heatmap.ts index d68a603d50..a3f056fec5 100644 --- a/packages/core/demo/data/heatmap.ts +++ b/packages/core/demo/data/heatmap.ts @@ -615,9 +615,10 @@ export const heatmapOptions = { scaleType: 'labels', }, }, - legend: { + heatmap: { colorLegend: { title: 'Legend title' }, }, + experimental: true, }; export const heatmapQuantizeLegendOption = { @@ -634,9 +635,10 @@ export const heatmapQuantizeLegendOption = { scaleType: 'labels', }, }, - legend: { + heatmap: { colorLegend: { title: 'Legend title', type: 'quantize' }, }, + experimental: true, }; export const heatmapDomainOptions = { @@ -667,9 +669,10 @@ export const heatmapDomainOptions = { ], }, }, - legend: { + heatmap: { colorLegend: { title: 'Legend title' }, }, + experimental: true, }; export const heatmapMissingData = [ @@ -1129,7 +1132,8 @@ export const heatmapMissingDataOptions = { scaleType: 'labels', }, }, - legend: { + heatmap: { colorLegend: { title: 'Legend title' }, }, + experimental: true, };