diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..1fd2638829 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-sunburst-percentage-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-percentage-visually-looks-correct-1-snap.png b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-percentage-visually-looks-correct-1-snap.png new file mode 100644 index 0000000000..0bbb125092 Binary files /dev/null and b/integration/tests/__image_snapshots__/all-test-ts-baseline-visual-tests-for-all-stories-treemap-percentage-visually-looks-correct-1-snap.png differ diff --git a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png index 2874bc10f6..1e26297411 100644 Binary files a/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png and b/integration/tests/__image_snapshots__/interactions-test-ts-tooltips-rotation-90-shows-tooltip-on-sunburst-1-snap.png differ diff --git a/src/chart_types/partition_chart/layout/config/config.ts b/src/chart_types/partition_chart/layout/config/config.ts index ec4712fa66..b65abda999 100644 --- a/src/chart_types/partition_chart/layout/config/config.ts +++ b/src/chart_types/partition_chart/layout/config/config.ts @@ -20,6 +20,8 @@ import { palettes } from '../../../../mocks/hierarchical/palettes'; import { Config, PartitionLayout, Numeric } from '../types/config_types'; import { GOLDEN_RATIO, TAU } from '../utils/math'; import { FONT_STYLES, FONT_VARIANTS } from '../types/types'; +import { ShapeTreeNode } from '../types/viewmodel_types'; +import { AGGREGATE_KEY, STATISTICS_KEY } from '../utils/group_by_rollup'; const log10 = Math.log(10); function significantDigitCount(d: number): number { @@ -29,18 +31,33 @@ function significantDigitCount(d: number): number { return Math.floor(Math.log(n) / log10) + 1; } -function defaultFormatter(d: any): string { - return typeof d === 'string' - ? d - : typeof d === 'number' - ? Math.abs(d) >= 10000000 || Math.abs(d) < 0.001 - ? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1))) - : d.toLocaleString(void 0, { - maximumSignificantDigits: 4, - maximumFractionDigits: 3, - useGrouping: true, - }) - : String(d); +export function sumValueGetter(node: ShapeTreeNode): number { + return node[AGGREGATE_KEY]; +} + +export function percentValueGetter(node: ShapeTreeNode): number { + return (100 * node[AGGREGATE_KEY]) / node.parent[STATISTICS_KEY].globalAggregate; +} + +export function ratioValueGetter(node: ShapeTreeNode): number { + return node[AGGREGATE_KEY] / node.parent[STATISTICS_KEY].globalAggregate; +} + +export const VALUE_GETTERS = Object.freeze({ percent: percentValueGetter, ratio: ratioValueGetter } as const); +export type ValueGetterName = keyof typeof VALUE_GETTERS; + +function defaultFormatter(d: number): string { + return Math.abs(d) >= 10000000 || Math.abs(d) < 0.001 + ? d.toExponential(Math.min(2, Math.max(0, significantDigitCount(d) - 1))) + : d.toLocaleString(void 0, { + maximumSignificantDigits: 4, + maximumFractionDigits: 3, + useGrouping: true, + }); +} + +export function percentFormatter(d: number): string { + return `${Math.round(d)}%`; } const valueFont = { @@ -154,6 +171,10 @@ export const configMetadata = { type: 'string', values: FONT_VARIANTS, }, + valueGetter: { + dflt: sumValueGetter, + type: 'function', + }, valueFormatter: { dflt: defaultFormatter, type: 'function', diff --git a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts index 0cdc20971f..db57301f73 100644 --- a/src/chart_types/partition_chart/layout/types/viewmodel_types.ts +++ b/src/chart_types/partition_chart/layout/types/viewmodel_types.ts @@ -19,7 +19,7 @@ import { Config } from './config_types'; import { Coordinate, Distance, Pixels, PointObject, PointTuple, Radian } from './geometry_types'; import { Font } from './types'; -import { config } from '../config/config'; +import { config, ValueGetterName } from '../config/config'; import { ArrayNode, HierarchyOfArrays } from '../utils/group_by_rollup'; import { Color } from '../../../../utils/commons'; @@ -124,4 +124,6 @@ export interface ShapeTreeNode extends TreeNode, SectorGeomSpecY { } export type RawTextGetter = (node: ShapeTreeNode) => string; +export type ValueGetterFunction = (node: ShapeTreeNode) => number; +export type ValueGetter = ValueGetterFunction | ValueGetterName; export type NodeColorAccessor = (d: ShapeTreeNode, index: number, array: HierarchyOfArrays) => string; diff --git a/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts b/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts index 202a68d49b..2d5f825a67 100644 --- a/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts +++ b/src/chart_types/partition_chart/layout/utils/group_by_rollup.ts @@ -19,16 +19,22 @@ import { Relation } from '../types/types'; import { Datum } from '../../../../utils/commons'; -export const AGGREGATE_KEY = 'value'; // todo later switch back to 'aggregate' +export const AGGREGATE_KEY = 'value'; +export const STATISTICS_KEY = 'statistics'; export const DEPTH_KEY = 'depth'; export const CHILDREN_KEY = 'children'; export const INPUT_KEY = 'inputIndex'; export const PARENT_KEY = 'parent'; export const SORT_INDEX_KEY = 'sortIndex'; +interface Statistics { + globalAggregate: number; +} + interface NodeDescriptor { [AGGREGATE_KEY]: number; [DEPTH_KEY]: number; + [STATISTICS_KEY]: Statistics; [INPUT_KEY]?: Array; } @@ -82,7 +88,10 @@ export function groupByRollup( identity: Function; }, factTable: Relation, -) { +): HierarchyOfMaps { + const statistics: Statistics = { + globalAggregate: NaN, + }; const reductionMap = factTable.reduce((p: HierarchyOfMaps, n, index) => { const keyCount = keyAccessors.length; let pointer: HierarchyOfMaps = p; @@ -97,6 +106,7 @@ export function groupByRollup( const reductionValue = reducer(aggregate, valueAccessor(n)); pointer.set(key, { [AGGREGATE_KEY]: reductionValue, + [STATISTICS_KEY]: statistics, [INPUT_KEY]: [...inputIndices, index], [DEPTH_KEY]: i, ...(!last && { [CHILDREN_KEY]: childrenMap }), @@ -108,6 +118,9 @@ export function groupByRollup( }); return p; }, new Map()); + if (reductionMap.get(null) !== void 0) { + statistics.globalAggregate = (reductionMap.get(null) as MapNode)[AGGREGATE_KEY]; + } return reductionMap; } @@ -127,6 +140,7 @@ export function mapsToArrays(root: HierarchyOfMaps, sorter: NodeSorter): Hierarc const valueElement = value[CHILDREN_KEY]; const resultNode: ArrayNode = { [AGGREGATE_KEY]: NaN, + [STATISTICS_KEY]: { globalAggregate: NaN }, [CHILDREN_KEY]: [], [DEPTH_KEY]: NaN, [SORT_INDEX_KEY]: NaN, diff --git a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts index 6fc88f41e0..ad033dfc7f 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/fill_text_layout.ts @@ -20,9 +20,16 @@ import { wrapToTau } from '../geometry'; import { Coordinate, Distance, Pixels, Radian, Radius, RingSector } from '../types/geometry_types'; import { Config } from '../types/config_types'; import { logarithm, TAU, trueBearingToStandardPositionAngle } from '../utils/math'; -import { QuadViewModel, RawTextGetter, RowBox, RowSet, RowSpace, ShapeTreeNode } from '../types/viewmodel_types'; +import { + QuadViewModel, + RawTextGetter, + RowBox, + RowSet, + RowSpace, + ShapeTreeNode, + ValueGetterFunction, +} from '../types/viewmodel_types'; import { Box, Font, PartialFont, TextMeasure } from '../types/types'; -import { AGGREGATE_KEY } from '../utils/group_by_rollup'; import { conjunctiveConstraint } from '../circline_geometry'; import { Layer } from '../../specs/index'; import { stringToRGB } from '../utils/d3_utils'; @@ -216,6 +223,7 @@ function identityRowSet(): RowSet { function getAllBoxes( rawTextGetter: RawTextGetter, + valueGetter: ValueGetterFunction, valueFormatter: ValueFormatter, sizeInvariantFontShorthand: Font, valueFont: PartialFont, @@ -225,7 +233,7 @@ function getAllBoxes( .split(' ') .map((text) => ({ text, ...sizeInvariantFontShorthand })) .concat( - valueFormatter(node[AGGREGATE_KEY]) + valueFormatter(valueGetter(node)) .split(' ') .map((text) => ({ text, ...sizeInvariantFontShorthand, ...valueFont })), ); @@ -241,7 +249,8 @@ function fill( fontSizes: string | any[], measure: TextMeasure, rawTextGetter: RawTextGetter, - formatter: (value: number) => string, + valueGetter: ValueGetterFunction, + formatter: ValueFormatter, textFillOrigins: any[], shapeConstructor: (n: ShapeTreeNode) => any, getShapeRowGeometry: (...args: any[]) => RowSpace, @@ -278,7 +287,7 @@ function fill( fontWeight, fontFamily, }; - const allBoxes = getAllBoxes(rawTextGetter, valueFormatter, sizeInvariantFont, valueFont, node); + const allBoxes = getAllBoxes(rawTextGetter, valueGetter, valueFormatter, sizeInvariantFont, valueFont, node); let rowSet = identityRowSet(); let completed = false; const rotation = getRotation(node); @@ -405,7 +414,8 @@ export function inSectorRotation(horizontalTextEnforcer: number, horizontalTextA export function fillTextLayout( measure: TextMeasure, rawTextGetter: RawTextGetter, - valueFormatter: (value: number) => string, + valueGetter: ValueGetterFunction, + valueFormatter: ValueFormatter, childNodes: QuadViewModel[], config: Config, layers: Layer[], @@ -433,6 +443,7 @@ export function fillTextLayout( fontSizes, measure, rawTextGetter, + valueGetter, valueFormatter, textFillOrigins, shapeConstructor, diff --git a/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts b/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts index efbe7ce594..552acae11a 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/link_text_layout.ts @@ -19,13 +19,11 @@ import { Distance } from '../types/geometry_types'; import { Config } from '../types/config_types'; import { TAU, trueBearingToStandardPositionAngle } from '../utils/math'; -import { LinkLabelVM, ShapeTreeNode } from '../types/viewmodel_types'; +import { LinkLabelVM, ShapeTreeNode, ValueGetterFunction } from '../types/viewmodel_types'; import { meanAngle } from '../geometry'; import { TextMeasure } from '../types/types'; -import { AGGREGATE_KEY } from '../utils/group_by_rollup'; import { ValueFormatter } from '../../../../utils/commons'; -// todo modularize this large function export function linkTextLayout( measure: TextMeasure, config: Config, @@ -33,6 +31,7 @@ export function linkTextLayout( currentY: Distance[], anchorRadius: Distance, rawTextGetter: Function, + valueGetter: ValueGetterFunction, valueFormatter: ValueFormatter, ): LinkLabelVM[] { const { linkLabel } = config; @@ -83,7 +82,7 @@ export function linkTextLayout( translate: [stemToX + west * (linkLabel.horizontalStemLength + linkLabel.gap), stemToY], textAlign: side ? 'left' : 'right', text, - valueText: valueFormatter(node[AGGREGATE_KEY]), + valueText: valueFormatter(valueGetter(node)), width, verticalOffset: -(emHeightDescent + emHeightAscent) / 2, // meaning, `middle` }; diff --git a/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts b/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts index 20055906ad..6d84789056 100644 --- a/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts +++ b/src/chart_types/partition_chart/layout/viewmodel/viewmodel.ts @@ -35,6 +35,7 @@ import { RowSet, ShapeTreeNode, ShapeViewModel, + ValueGetterFunction, } from '../types/viewmodel_types'; import { Layer } from '../../specs/index'; import { @@ -61,7 +62,8 @@ import { parentAccessor, sortIndexAccessor, } from '../utils/group_by_rollup'; -import { ValueAccessor } from '../../../../utils/commons'; +import { ValueAccessor, ValueFormatter } from '../../../../utils/commons'; +import { percentValueGetter } from '../config/config'; function paddingAccessor(n: ArrayEntry) { return entryValue(n).depth > 1 ? 1 : [0, 2][entryValue(n).depth]; @@ -145,7 +147,9 @@ export function shapeViewModel( facts: Relation, rawTextGetter: RawTextGetter, valueAccessor: ValueAccessor, - valueFormatter: (value: number) => string, + specifiedValueFormatter: ValueFormatter, + specifiedPercentFormatter: ValueFormatter, + valueGetter: ValueGetterFunction, groupByRollupAccessors: IndexedAccessorFn[], ): ShapeViewModel { const { @@ -247,9 +251,12 @@ export function shapeViewModel( const textFillOrigins = nodesWithRoom.map(treemapLayout ? rectangleFillOrigins : sectorFillOrigins(fillOutside)); + const valueFormatter = valueGetter === percentValueGetter ? specifiedPercentFormatter : specifiedValueFormatter; + const rowSets: RowSet[] = fillTextLayout( textMeasure, rawTextGetter, + valueGetter, valueFormatter, nodesWithRoom, config, @@ -283,6 +290,7 @@ export function shapeViewModel( currentY, outerRadius, rawTextGetter, + valueGetter, valueFormatter, ); diff --git a/src/chart_types/partition_chart/specs/index.ts b/src/chart_types/partition_chart/specs/index.ts index 86d02a83e6..45189e6524 100644 --- a/src/chart_types/partition_chart/specs/index.ts +++ b/src/chart_types/partition_chart/specs/index.ts @@ -17,12 +17,14 @@ * under the License. */ import { ChartTypes } from '../../index'; -import { config } from '../layout/config/config'; +import { config, percentFormatter } from '../layout/config/config'; import { FunctionComponent } from 'react'; import { getConnect, specComponentFactory } from '../../../state/spec_factory'; import { IndexedAccessorFn } from '../../../utils/accessor'; import { Spec, SpecTypes } from '../../../specs/index'; import { Config, FillLabelConfig } from '../layout/types/config_types'; +import { ShapeTreeNode, ValueGetter } from '../layout/types/viewmodel_types'; +import { AGGREGATE_KEY } from '../layout/utils/group_by_rollup'; import { Datum, LabelAccessor, RecursivePartial, ValueAccessor, ValueFormatter } from '../../../utils/commons'; import { NodeColorAccessor } from '../layout/types/viewmodel_types'; import { PrimitiveValue } from '../layout/utils/group_by_rollup'; @@ -39,7 +41,9 @@ const defaultProps = { specType: SpecTypes.Series, config, valueAccessor: (d: Datum) => (typeof d === 'number' ? d : 0), + valueGetter: (n: ShapeTreeNode): number => n[AGGREGATE_KEY], valueFormatter: (d: number): string => String(d), + percentFormatter, layers: [ { groupByRollup: (d: Datum, i: number) => i, @@ -56,6 +60,8 @@ export interface PartitionSpec extends Spec { data: Datum[]; valueAccessor: ValueAccessor; valueFormatter: ValueFormatter; + valueGetter: ValueGetter; + percentFormatter: ValueFormatter; layers: Layer[]; } @@ -63,5 +69,8 @@ type SpecRequiredProps = Pick; type SpecOptionalProps = Partial>; export const Partition: FunctionComponent = getConnect()( - specComponentFactory(defaultProps), + specComponentFactory< + PartitionSpec, + 'valueAccessor' | 'valueGetter' | 'valueFormatter' | 'layers' | 'config' | 'percentFormatter' + >(defaultProps), ); diff --git a/src/chart_types/partition_chart/state/selectors/scenegraph.ts b/src/chart_types/partition_chart/state/selectors/scenegraph.ts index 98f7344f0f..63efd15c0b 100644 --- a/src/chart_types/partition_chart/state/selectors/scenegraph.ts +++ b/src/chart_types/partition_chart/state/selectors/scenegraph.ts @@ -19,11 +19,17 @@ import { Dimensions } from '../../../../utils/dimensions'; import { shapeViewModel } from '../../layout/viewmodel/viewmodel'; import { measureText } from '../../layout/utils/measure'; -import { ShapeTreeNode, ShapeViewModel, RawTextGetter, nullShapeViewModel } from '../../layout/types/viewmodel_types'; +import { + ShapeTreeNode, + ShapeViewModel, + RawTextGetter, + nullShapeViewModel, + ValueGetter, +} from '../../layout/types/viewmodel_types'; import { DEPTH_KEY } from '../../layout/utils/group_by_rollup'; import { PartitionSpec, Layer } from '../../specs/index'; import { identity, mergePartial, RecursivePartial } from '../../../../utils/commons'; -import { config as defaultConfig } from '../../layout/config/config'; +import { config as defaultConfig, VALUE_GETTERS } from '../../layout/config/config'; import { Config } from '../../layout/types/config_types'; function rawTextGetter(layers: Layer[]): RawTextGetter { @@ -33,6 +39,10 @@ function rawTextGetter(layers: Layer[]): RawTextGetter { }; } +export function valueGetterFunction(valueGetter: ValueGetter) { + return typeof valueGetter === 'function' ? valueGetter : VALUE_GETTERS[valueGetter]; +} + export function render(partitionSpec: PartitionSpec, parentDimensions: Dimensions): ShapeViewModel { const { width, height } = parentDimensions; const { layers, data: facts, config: specConfig } = partitionSpec; @@ -43,6 +53,7 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension if (!textMeasurerCtx) { return nullShapeViewModel(config, { x: width / 2, y: height / 2 }); } + const valueGetter = valueGetterFunction(partitionSpec.valueGetter); return shapeViewModel( measureText(textMeasurerCtx), config, @@ -51,6 +62,8 @@ export function render(partitionSpec: PartitionSpec, parentDimensions: Dimension rawTextGetter(layers), partitionSpec.valueAccessor, partitionSpec.valueFormatter, + partitionSpec.percentFormatter, + valueGetter, [() => null, ...layers.map(({ groupByRollup }) => groupByRollup)], ); } diff --git a/src/chart_types/partition_chart/state/selectors/tooltip.ts b/src/chart_types/partition_chart/state/selectors/tooltip.ts index 80ad02e6ad..199b8f6d8f 100644 --- a/src/chart_types/partition_chart/state/selectors/tooltip.ts +++ b/src/chart_types/partition_chart/state/selectors/tooltip.ts @@ -17,40 +17,38 @@ * under the License. */ import createCachedSelector from 're-reselect'; -import { GlobalChartState } from '../../../../state/chart_state'; -import { INPUT_KEY } from '../../layout/utils/group_by_rollup'; import { TooltipInfo } from '../../../../components/tooltip/types'; +import { valueGetterFunction } from './scenegraph'; +import { percentValueGetter, sumValueGetter } from '../../layout/config/config'; import { getPieSpecOrNull } from './pie_spec'; import { getPickedShapes } from './picked_shapes'; -function getValueFormatter(state: GlobalChartState) { - return getPieSpecOrNull(state)?.valueFormatter; -} - -function getLabelFormatters(state: GlobalChartState) { - return getPieSpecOrNull(state)?.layers; -} - const EMPTY_TOOLTIP = Object.freeze({ header: null, values: [], }); export const getTooltipInfoSelector = createCachedSelector( - [getPieSpecOrNull, getPickedShapes, getValueFormatter, getLabelFormatters], - (pieSpec, pickedShapes, valueFormatter, labelFormatters): TooltipInfo => { - if (!pieSpec || !valueFormatter || !labelFormatters) { + [getPieSpecOrNull, getPickedShapes], + (pieSpec, pickedShapes): TooltipInfo => { + if (!pieSpec) { + return EMPTY_TOOLTIP; + } + const { valueGetter, valueFormatter, layers: labelFormatters } = pieSpec; + if (!valueFormatter || !labelFormatters) { return EMPTY_TOOLTIP; } - const datumIndices = new Set(); const tooltipInfo: TooltipInfo = { header: null, values: [], }; + + const valueGetterFun = valueGetterFunction(valueGetter); + const primaryValueGetterFun = valueGetterFun === percentValueGetter ? sumValueGetter : valueGetterFun; pickedShapes.forEach((shape) => { - const node = shape.parent; - const formatter = labelFormatters[shape.depth - 1] && labelFormatters[shape.depth - 1].nodeLabel; + const labelFormatter = labelFormatters[shape.depth - 1]; + const formatter = labelFormatter?.nodeLabel; tooltipInfo.values.push({ label: formatter ? formatter(shape.dataName) : shape.dataName, @@ -61,13 +59,11 @@ export const getTooltipInfoSelector = createCachedSelector( specId: pieSpec.id, key: pieSpec.id, }, - value: valueFormatter(shape.value), + value: `${valueFormatter(primaryValueGetterFun(shape))} (${pieSpec.percentFormatter( + percentValueGetter(shape), + )})`, + valueAccessor: shape.depth, }); - const shapeNode = node.children.find(([key]) => key === shape.dataName); - if (shapeNode) { - const indices = shapeNode[1][INPUT_KEY] || []; - indices.forEach((i) => datumIndices.add(i)); - } }); return tooltipInfo; diff --git a/stories/sunburst/26_percentage.tsx b/stories/sunburst/26_percentage.tsx new file mode 100644 index 0000000000..385bf8b31b --- /dev/null +++ b/stories/sunburst/26_percentage.tsx @@ -0,0 +1,97 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import React from 'react'; +import { ShapeTreeNode } from '../../src/chart_types/partition_chart/layout/types/viewmodel_types'; +import { + categoricalFillColor, + colorBrewerCategoricalStark9, + countryLookup, + productLookup, + regionLookup, +} from '../utils/utils'; +import { config } from '../../src/chart_types/partition_chart/layout/config/config'; + +export const example = () => ( + + d.exportVal as number} + valueGetter="percent" + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + percentFormatter={(d: number) => `${Math.round((d + Number.EPSILON) * 100) / 100}%`} + layers={[ + { + groupByRollup: (d: Datum) => d.sitc1, + nodeLabel: (d: any) => productLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.7)(d.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d: any) => regionLookup[d].regionName, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.5)(d.parent.sortIndex); + }, + }, + }, + { + groupByRollup: (d: Datum) => d.dest, + nodeLabel: (d: any) => countryLookup[d].name, + shape: { + fillColor: (d: ShapeTreeNode) => { + return categoricalFillColor(colorBrewerCategoricalStark9, 0.3)(d.parent.parent.sortIndex); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.sunburst, + linkLabel: { + maxCount: 0, + fontSize: 14, + }, + fontFamily: 'Arial', + fillLabel: { + fontStyle: 'italic', + textInvertible: true, + fontWeight: 900, + valueFont: { + fontFamily: 'Menlo', + fontStyle: 'normal', + fontWeight: 100, + }, + }, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 1, + idealFontSizeJump: 1.1, + outerSizeRatio: 1, + emptySizeRatio: 0, + circlePadding: 4, + backgroundColor: 'rgba(229,229,229,1)', + }} + /> + +); diff --git a/stories/sunburst/sunburst.stories.tsx b/stories/sunburst/sunburst.stories.tsx index 2dce0b5dc6..2568620122 100644 --- a/stories/sunburst/sunburst.stories.tsx +++ b/stories/sunburst/sunburst.stories.tsx @@ -52,3 +52,4 @@ export { example as counterClockwiseSpecial } from './22_counter_clockwise'; export { example as clockwiseNoSpecial } from './23_clockwise'; export { example as linkedLabelsOnly } from './24_linked_label'; export { example as noLabels } from './25_no_labels'; +export { example as percentage } from './26_percentage'; diff --git a/stories/treemap/5_multicolor.tsx b/stories/treemap/5_multicolor.tsx index c09bde9bdf..0b4abf612f 100644 --- a/stories/treemap/5_multicolor.tsx +++ b/stories/treemap/5_multicolor.tsx @@ -23,6 +23,7 @@ import { arrayToLookup, hueInterpolator } from '../../src/chart_types/partition_ import { countryDimension } from '../../src/mocks/hierarchical/dimension_codes'; import { palettes } from '../../src/mocks/hierarchical/palettes'; import React from 'react'; +import { regionLookup } from '../utils/utils'; const countryLookup = arrayToLookup((d: Datum) => d.country, countryDimension); @@ -48,9 +49,10 @@ export const example = () => ( layers={[ { groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), - nodeLabel: () => '', + nodeLabel: (d: any) => regionLookup[d].regionName, fillLabel: { valueFormatter: () => '', + textColor: 'rgba(0,0,0,0)', }, shape: { fillColor: defaultFillColor(interpolatorCET2s), diff --git a/stories/treemap/6_custom_style.tsx b/stories/treemap/6_custom_style.tsx index bc4ff607b3..43b00af4dd 100644 --- a/stories/treemap/6_custom_style.tsx +++ b/stories/treemap/6_custom_style.tsx @@ -22,6 +22,7 @@ import { config } from '../../src/chart_types/partition_chart/layout/config/conf import { arrayToLookup } from '../../src/chart_types/partition_chart/layout/utils/calcs'; import { countryDimension } from '../../src/mocks/hierarchical/dimension_codes'; import React from 'react'; +import { regionLookup } from '../utils/utils'; const countryLookup = arrayToLookup((d: Datum) => d.country, countryDimension); @@ -42,9 +43,10 @@ export const example = () => ( layers={[ { groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), - nodeLabel: () => '', + nodeLabel: (d: any) => regionLookup[d].regionName, fillLabel: { valueFormatter: () => '', + textColor: 'rgba(0,0,0,0)', }, shape: { fillColor: (d: any, i: any, a: any) => { diff --git a/stories/treemap/7_percentage.tsx b/stories/treemap/7_percentage.tsx new file mode 100644 index 0000000000..ff38888043 --- /dev/null +++ b/stories/treemap/7_percentage.tsx @@ -0,0 +1,91 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + +import { Chart, Datum, Partition, PartitionLayout } from '../../src'; +import { mocks } from '../../src/mocks/hierarchical/index'; +import { config, percentValueGetter } from '../../src/chart_types/partition_chart/layout/config/config'; +import { arrayToLookup, hueInterpolator } from '../../src/chart_types/partition_chart/layout/utils/calcs'; +import { countryDimension, regionDimension } from '../../src/mocks/hierarchical/dimension_codes'; +import { palettes } from '../../src/mocks/hierarchical/palettes'; +import React from 'react'; +import { ShapeTreeNode } from '../../src/chart_types/partition_chart/layout/types/viewmodel_types'; + +const regionLookup = arrayToLookup((d: Datum) => d.region, regionDimension); +const countryLookup = arrayToLookup((d: Datum) => d.country, countryDimension); + +const interpolatorTurbo = hueInterpolator(palettes.turbo.map(([r, g, b]) => [r, g, b, 0.7])); + +export const example = () => ( + + d.exportVal as number} + valueGetter={percentValueGetter} + valueFormatter={(d: number) => `$${config.fillLabel.valueFormatter(Math.round(d / 1000000000))}\xa0Bn`} + layers={[ + { + groupByRollup: (d: Datum) => countryLookup[d.dest].continentCountry.substr(0, 2), + nodeLabel: (d: any) => regionLookup[d].regionName, + fillLabel: { + fontFamily: 'Phosphate-Inline', + textColor: 'yellow', + textInvertible: false, + }, + shape: { fillColor: 'rgba(0,0,0,0)' }, + }, + { + groupByRollup: (d: Datum) => d.dest, + nodeLabel: (d: any) => countryLookup[d].name, + fillLabel: { + textColor: 'black', + textInvertible: false, + fontWeight: 200, + fontStyle: 'normal', + fontFamily: 'Helvetica', + fontVariant: 'small-caps', + valueFont: { fontWeight: 400, fontStyle: 'italic' }, + }, + shape: { + fillColor: (d: ShapeTreeNode) => { + // primarily, pick color based on parent's index, but then perturb by the index within the parent + return interpolatorTurbo( + (d.parent.sortIndex + d.sortIndex / d.parent.children.length) / (d.parent.parent.children.length + 1), + ); + }, + }, + }, + ]} + config={{ + partitionLayout: PartitionLayout.treemap, + margin: { top: 0, bottom: 0, left: 0, right: 0 }, + minFontSize: 4, + maxFontSize: 84, + idealFontSizeJump: 1.15, + outerSizeRatio: 1, + }} + /> + +); diff --git a/stories/treemap/treemap.stories.tsx b/stories/treemap/treemap.stories.tsx index 4d9a641a86..fbd833b203 100644 --- a/stories/treemap/treemap.stories.tsx +++ b/stories/treemap/treemap.stories.tsx @@ -31,3 +31,4 @@ export { example as midTwoLayers } from './3_mid_two'; export { example as twoLayersStressTest } from './4_two_layer_stress'; export { example as multiColor } from './5_multicolor'; export { example as customStyle } from './6_custom_style'; +export { example as percentage } from './7_percentage';