diff --git a/src/alpha/components/Treemap/Treemap.tsx b/src/alpha/components/Treemap/Treemap.tsx new file mode 100644 index 000000000..9955ff3e1 --- /dev/null +++ b/src/alpha/components/Treemap/Treemap.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { FC } from 'react'; + +import { TreemapProps } from '../../../types'; + +const Treemap: FC = ({ + color, + segmentKey, + colorScheme, + paddingInner, + paddingOuter, + aspectRatio, + layout, + colorScaleType, + children, + ...props +}) => { + return null; +}; + +// displayName is used to validate the component type in the spec builder +Treemap.displayName = 'Treemap'; + +export { Treemap }; diff --git a/src/alpha/components/Treemap/index.ts b/src/alpha/components/Treemap/index.ts new file mode 100644 index 000000000..564c0eaf1 --- /dev/null +++ b/src/alpha/components/Treemap/index.ts @@ -0,0 +1,13 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +export * from './Treemap'; diff --git a/src/alpha/components/index.ts b/src/alpha/components/index.ts index e29b443f4..cd6af4fba 100644 --- a/src/alpha/components/index.ts +++ b/src/alpha/components/index.ts @@ -10,4 +10,5 @@ * governing permissions and limitations under the License. */ -export * from './Combo' \ No newline at end of file +export * from './Combo'; +export * from './Treemap'; diff --git a/src/specBuilder/chartSpecBuilder.ts b/src/specBuilder/chartSpecBuilder.ts index a141fe163..3688c8e57 100644 --- a/src/specBuilder/chartSpecBuilder.ts +++ b/src/specBuilder/chartSpecBuilder.ts @@ -29,7 +29,7 @@ import { TABLE, } from '@constants'; import { Area, Axis, Bar, Legend, Line, Scatter, Title } from '@rsc'; -import { Combo } from '@rsc/alpha'; +import { Combo, Treemap } from '@rsc/alpha'; import { BigNumber, Donut } from '@rsc/rc'; import colorSchemes from '@themes/colorSchemes'; import { produce } from 'immer'; @@ -57,6 +57,7 @@ import { SymbolShapes, SymbolSize, TitleElement, + TreemapElement, } from '../types'; import { addArea } from './area/areaSpecBuilder'; import { addAxis } from './axis/axisSpecBuilder'; @@ -81,6 +82,7 @@ import { initializeSpec, } from './specUtils'; import { addTitle } from './title/titleSpecBuilder'; +import { addTreemap } from './treemap/treemapSpecBuilder'; export function buildSpec(props: SanitizedSpecProps) { const { @@ -112,11 +114,12 @@ export function buildSpec(props: SanitizedSpecProps) { buildOrder.set(Donut, 0); buildOrder.set(Scatter, 0); buildOrder.set(Combo, 0); + buildOrder.set(Treemap, 0); buildOrder.set(Legend, 1); buildOrder.set(Axis, 2); buildOrder.set(Title, 3); - let { areaCount, axisCount, barCount, comboCount, donutCount, legendCount, lineCount, scatterCount } = + let { areaCount, axisCount, barCount, comboCount, donutCount, treemapCount, legendCount, lineCount, scatterCount } = initializeComponentCounts(); const specProps = { colorScheme, idKey, highlightedItem }; spec = [...children] @@ -144,6 +147,9 @@ export function buildSpec(props: SanitizedSpecProps) { case Donut.displayName: donutCount++; return addDonut(acc, { ...(cur as DonutElement).props, ...specProps, index: donutCount }); + case Treemap.displayName: + treemapCount++; + return addTreemap(acc, { ...(cur as TreemapElement).props, ...specProps, index: treemapCount }); case Legend.displayName: legendCount++; return addLegend(acc, { @@ -202,6 +208,7 @@ const initializeComponentCounts = () => { barCount: -1, comboCount: -1, donutCount: -1, + treemapCount: -1, legendCount: -1, lineCount: -1, scatterCount: -1, diff --git a/src/specBuilder/treemap/treemapSpecBuilder.ts b/src/specBuilder/treemap/treemapSpecBuilder.ts new file mode 100644 index 000000000..576e380da --- /dev/null +++ b/src/specBuilder/treemap/treemapSpecBuilder.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { COLOR_SCALE, DEFAULT_COLOR, DEFAULT_COLOR_SCHEME, DEFAULT_METRIC, TABLE } from '@constants'; +import { addFieldToFacetScaleDomain } from '@specBuilder/scale/scaleSpecBuilder'; +import { sanitizeMarkChildren, toCamelCase } from '@utils'; +import { produce } from 'immer'; +import { LinearScale, StratifyTransform } from 'vega'; +import { Data, Mark, Scale, Spec, TreemapTransform } from 'vega'; + +import { ColorScheme, HighlightedItem, TreemapProps, TreemapSpecProps } from '../../types'; +import { getLeavesMarks, getLeavesText, getNodeMarks, getNodesText, getRootText } from './treemapUtils'; + +export const addTreemap = produce< + Spec, + [TreemapProps & { colorScheme?: ColorScheme; highlightedItem?: HighlightedItem; index?: number; idKey: string }] +>( + ( + spec, + { + children, + color = DEFAULT_COLOR, + colorScheme = DEFAULT_COLOR_SCHEME, + index = 0, + metric = DEFAULT_METRIC, + name, + segmentKey = 'segment', + layout = 'binary', + ...props + } + ) => { + const treemapProps: TreemapSpecProps = { + children: sanitizeMarkChildren(children), + color, + colorScheme, + index, + metric, + segmentKey, + name: toCamelCase(name ?? `treemap${index}`), + layout, + ...props, + }; + + // need to add the treemap to the spec + spec.data = addData(spec.data ?? [], treemapProps); + spec.scales = addScales(spec.scales ?? [], treemapProps); + spec.marks = addMarks(spec.marks ?? [], treemapProps); + } +); + +export const addData = produce((data, props) => { + // const { children, idKey, metric } = props; + // console.log('props', { props, data, TABLE }); + const tableIndex = data.findIndex((d) => d.name === TABLE); + + // setup transform + data[tableIndex].transform = data[tableIndex].transform ?? []; + data[tableIndex].transform?.push(...getTreemapTransforms(props)); + data.push({ + name: 'nodes', + source: TABLE, + transform: [{ type: 'filter', expr: 'datum.children' }], + }); + data.push({ + name: 'leaves', + source: TABLE, + transform: [{ type: 'filter', expr: '!datum.children' }], + }); + data.push({ + name: 'trunk', + source: TABLE, + transform: [{ type: 'filter', expr: '!datum.parent' }], + }); +}); + +export const getTreemapTransforms = ({ + idKey, + parent, + paddingInner, + paddingOuter, + aspectRatio, + layout, +}: TreemapSpecProps): (StratifyTransform | TreemapTransform)[] => [ + { + type: 'stratify', + key: idKey, + parentKey: parent ?? 'parent', + }, + { + type: 'treemap', + field: 'size', + sort: { field: 'value' }, + paddingInner: paddingInner ?? 1, + paddingOuter: paddingOuter ?? 1, + round: true, + method: layout ?? 'squarify', + ratio: aspectRatio ?? 1, + size: [{ signal: 'width' }, { signal: 'height' }], + }, +]; + +export const addScales = produce((scales, props) => { + const { segmentKey } = props; + addFieldToFacetScaleDomain(scales, COLOR_SCALE, segmentKey); + scales.push(getTreemapOpacityScales(props)); +}); + +export const getTreemapOpacityScales = ({}: TreemapSpecProps): LinearScale => ({ + name: 'opacityScale', + type: 'linear', + domain: [2, 3, 4, 5, 6, 7], + range: [0.75, 1, 0.75, 1, 0.75, 1], +}); + +export const addMarks = produce((marks, props) => { + marks.push(getNodeMarks(props)); + marks.push(getLeavesMarks(props)); + marks.push(getRootText(props)); + marks.push(getNodesText(props)); + marks.push(getLeavesText(props)); +}); diff --git a/src/specBuilder/treemap/treemapUtils.ts b/src/specBuilder/treemap/treemapUtils.ts new file mode 100644 index 000000000..4c2af8b02 --- /dev/null +++ b/src/specBuilder/treemap/treemapUtils.ts @@ -0,0 +1,191 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { RectMark, TextMark } from 'vega'; + +import { TreemapSpecProps } from '../../types'; + +export const getNodeMarks = (props: TreemapSpecProps): RectMark => { + const { segmentKey, borderColor, nodesBorderWidth } = props; + return { + type: 'rect', + from: { data: 'nodes' }, + interactive: false, + encode: { + enter: { + stroke: { value: borderColor ?? 'rgb(82, 80, 82)' }, + strokeWidth: { value: nodesBorderWidth ?? 5 }, + fill: { scale: 'color', field: segmentKey ?? 'segment' }, + tooltip: { + signal: "'Name: ' + datum.name + ', depth: ' + datum.depth + ', value: ' + datum.size", + }, + fillOpacity: { scale: 'opacityScale', field: 'depth' }, + }, + update: { + x: { field: 'x0' }, + y: { field: 'y0' }, + x2: { field: 'x1' }, + y2: { field: 'y1' }, + }, + hover: { + fill: { scale: 'color', field: segmentKey ?? 'segment' }, + fillOpacity: { value: 0.5 }, + }, + }, + }; +}; + +export const getLeavesMarks = (props: TreemapSpecProps): RectMark => { + const { color, segmentKey, leavesBorderWidth } = props; + + return { + type: 'rect', + from: { data: 'leaves' }, + encode: { + enter: { + stroke: { value: '#4b4848' }, + strokeWidth: { value: leavesBorderWidth ?? 1 }, + fill: { scale: color ?? 'color', field: segmentKey ?? 'segment' }, + tooltip: { + signal: "'Name: ' + datum.name + ', depth: ' + datum.depth + ', value: ' + datum.size", + }, + fillOpacity: { scale: 'opacityScale', field: 'depth' }, + }, + update: { + x: { field: 'x0' }, + y: { field: 'y0' }, + x2: { field: 'x1' }, + y2: { field: 'y1' }, + fill: { value: 'transparent' }, + }, + hover: { + fill: { scale: 'color', field: segmentKey ?? 'segment' }, + fillOpacity: { value: 1 }, + }, + }, + }; +}; + +export const getRootText = (props: TreemapSpecProps): TextMark => { + const { rootTextColor } = props; + + return { + type: 'text', + name: 'trunkText', + from: { data: 'trunk' }, + interactive: false, + encode: { + enter: { + font: { value: 'Helvetica Neue, Arial' }, + x: { signal: '(datum.x0 + datum.x1) / 2' }, + y: { signal: '(datum.y0 + datum.y1) / 2' }, + align: { value: 'center' }, + baseline: { value: 'middle' }, + fill: { value: rootTextColor ?? '#2e2525' }, + text: { field: 'name' }, // not sure why using name prop doesn't apply styling + fontSize: { value: 72 }, + fillOpacity: { value: 0.5 }, + }, + update: { + x: { signal: 'width / 2' }, + y: { signal: 'height / 2' }, + }, + }, + }; +}; + +export const getNodesText = (props: TreemapSpecProps): TextMark => { + const { textColor } = props; + + return { + type: 'text', + name: 'nodesText', + from: { data: 'nodes' }, + interactive: false, + encode: { + enter: { + font: { value: 'Helvetica Neue, Arial' }, + x: { signal: '(datum.x0 + datum.x1) / 2' }, + y: { signal: '(datum.y0 + datum.y1) / 2' }, + align: { value: 'center' }, + baseline: { value: 'alphabetical', scale: 'color' }, + fill: { value: textColor ?? '#f8f5f5' }, + text: { field: 'name' }, + fillOpacity: { field: 'value' }, + fontSize: { + signal: '((datum.x1 - datum.x0 > 40) && (datum.y1 - datum.y0 > 20)) ? clamp((datum.x1 - datum.x0) * 0.2, 10, 20) : 0', + }, + tooltip: { + signal: "'Name: ' + datum.name + ', depth: ' + datum.depth + ', value: ' + datum.size", + }, + }, + update: { + x: { signal: '0.5 * (datum.x0 + datum.x1)' }, + y: { signal: '0.5 * (datum.y0 + datum.y1)' }, + }, + }, + transform: [ + { + type: 'label', + avoidMarks: ['trunkText'], + avoidBaseMark: false, + // anchor: ['middle'], + // padding: 5, + // offset: [5, 5], // still have some node text overlapping, need to fix that + size: { signal: '[width, height]' }, + }, + ], + }; +}; + +export const getLeavesText = (props: TreemapSpecProps): TextMark => { + const { textColor } = props; + + return { + type: 'text', + name: 'leavesText', + from: { data: 'leaves' }, + interactive: true, + encode: { + enter: { + font: { value: 'Helvetica Neue, Arial' }, + x: { signal: '(datum.x0 + datum.x1) / 2' }, + y: { signal: 'datum.y0 + (datum.y1 - datum.y0) * 0.5 - 2' }, + align: { value: 'center' }, + baseline: { value: 'middle' }, + fill: { value: textColor ?? '#f3efef' }, + text: { field: 'name' }, + limit: { signal: 'datum.x1 - datum.x0 > 40 ? (datum.x1 - datum.x0) * 0.9 : datum.x1 - datum.x0 - 2' }, + width: { signal: 'datum.x1 - datum.x0' }, + fillOpacity: { scale: 'opacity', field: 'depth' }, + fontSize: { + signal: '((datum.x1 - datum.x0 > 40) && (datum.y1 - datum.y0 > 20)) ? clamp((datum.x1 - datum.x0) * 0.1, 10, 20) : 0', + }, + tooltip: { + signal: "'Name: ' + datum.name + ', depth: ' + datum.depth + ', value: ' + datum.size", + }, + }, + update: { + x: { signal: '0.5 * (datum.x0 + datum.x1)' }, + y: { signal: '0.5 * (datum.y0 + datum.y1) - 2' }, // Adding slight padding + }, + }, + transform: [ + { + type: 'label', + avoidBaseMark: false, + avoidMarks: ['trunkText', 'nodesText'], + anchor: ['middle'], + size: { signal: '[width, height]' }, + }, + ], + }; +}; diff --git a/src/stories/components/Treemap/Treemap.story.tsx b/src/stories/components/Treemap/Treemap.story.tsx new file mode 100644 index 000000000..4ab304070 --- /dev/null +++ b/src/stories/components/Treemap/Treemap.story.tsx @@ -0,0 +1,84 @@ +/* + * Copyright 2023 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { ReactElement } from 'react'; + +import useChartProps from '@hooks/useChartProps'; +import { Chart, ChartProps, SpectrumColor, TreemapProps } from '@rsc'; +import { Treemap } from '@rsc/alpha'; +import { StoryFn } from '@storybook/react'; +import { bindWithProps } from '@test-utils'; + +import { basicTreemapData } from './data'; + +export default { + title: 'RSC/Treemap', + component: Treemap, +}; + +const defaultChartProps: ChartProps = { + data: basicTreemapData, + width: 600, + height: 600, +}; + +const colors: SpectrumColor[] = [ + 'sequential-magma-200', + 'sequential-magma-400', + 'sequential-magma-600', + 'sequential-magma-800', + 'sequential-magma-1000', + 'sequential-magma-1200', + 'sequential-magma-1400', + 'sequential-magma-1600', +]; + +const TreemapStory: StoryFn = (args): ReactElement => { + const { width, height, ...treemapProps } = args; + + const chartProps = useChartProps({ ...defaultChartProps, colors, width: width ?? 600, height: height ?? 600 }); + return ( + + + + ); +}; + +// TODO: add component props and additional stories here + +const Basic = bindWithProps(TreemapStory); +Basic.args = { + layout: 'squarify', +}; + +const Binary = bindWithProps(TreemapStory); +Binary.args = { + layout: 'binary', +}; + +const SliceDice = bindWithProps(TreemapStory); +SliceDice.args = { + layout: 'slicedice', +}; + +const HighAspectRatio = bindWithProps(TreemapStory); +HighAspectRatio.args = { + layout: 'squarify', + aspectRatio: 1.5, +}; + +const LowAspectRatio = bindWithProps(TreemapStory); +LowAspectRatio.args = { + layout: 'squarify', + aspectRatio: 0.5, +}; + +export { Basic, Binary, SliceDice, HighAspectRatio, LowAspectRatio }; diff --git a/src/stories/components/Treemap/Treemap.test.tsx b/src/stories/components/Treemap/Treemap.test.tsx new file mode 100644 index 000000000..a5045899b --- /dev/null +++ b/src/stories/components/Treemap/Treemap.test.tsx @@ -0,0 +1,32 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ +import { Treemap } from '@rsc/alpha'; +import { findChart, render } from '@test-utils'; + +import { Basic } from './Treemap.story'; + +describe('Donut', () => { + // Donut is not a real React component. This is test just provides test coverage for sonarqube + test('Donut pseudo element', () => { + render(); + }); + + test('Basic renders properly', async () => { + render(); + const chart = await findChart(); + expect(chart).toBeInTheDocument(); + + // tree data has 7 segments?? + // const bars = await findAllMarksByGroupName(chart, 'treemap0'); + // expect(bars.length).toEqual(7); + }); +}); diff --git a/src/stories/components/Treemap/data.ts b/src/stories/components/Treemap/data.ts new file mode 100644 index 000000000..f163b4228 --- /dev/null +++ b/src/stories/components/Treemap/data.ts @@ -0,0 +1,468 @@ +/* + * Copyright 2024 Adobe. All rights reserved. + * This file is licensed 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 REPRESENTATIONS + * OF ANY KIND, either express or implied. See the License for the specific language + * governing permissions and limitations under the License. + */ + +// export const basicTreemapData = [ +// { id: 'Root', name: 'Company Sales', size: 0 }, +// { id: 'Region1', parentId: 'Root', name: 'North America', size: 0 }, +// { id: 'Region2', parentId: 'Root', name: 'Europe', size: 0 }, +// { id: 'Region3', parentId: 'Root', name: 'Asia', size: 0 }, +// { id: 'Category1', parentId: 'Region1', name: 'Electronics', size: 0 }, +// { id: 'Category2', parentId: 'Region1', name: 'Furniture', size: 0 }, +// { id: 'Category3', parentId: 'Region2', name: 'Clothing', size: 0 }, +// { id: 'Category4', parentId: 'Region3', name: 'Beauty', size: 0 }, +// { id: 'Product1', parentId: 'Category1', name: 'Smartphones', size: 120000 }, +// { id: 'Product2', parentId: 'Category1', name: 'Laptops', size: 80000 }, +// { id: 'Product3', parentId: 'Category2', name: 'Chairs', size: 45000 }, +// { id: 'Product4', parentId: 'Category2', name: 'Tables', size: 30000 }, +// { id: 'Product5', parentId: 'Category3', name: "Men's Wear", size: 50000 }, +// { id: 'Product6', parentId: 'Category3', name: "Women's Wear", size: 70000 }, +// { id: 'Product7', parentId: 'Category4', name: 'Skincare', size: 65000 }, +// { id: 'Product8', parentId: 'Category4', name: 'Makeup', size: 55000 }, +// ]; + +export const basicTreemapData = [ + { + id: 1, + name: 'flare', + }, + { + id: 2, + name: 'analytics', + parent: 1, + segment: 'analytics', + }, + { + id: 3, + name: 'cluster', + parent: 2, + segment: 'analytics', + }, + { + id: 4, + name: 'AgglomerativeCluster', + parent: 3, + segment: 'analytics', + size: 3938, + }, + { + id: 5, + name: 'CommunityStructure', + parent: 3, + segment: 'analytics', + size: 3812, + }, + { + id: 6, + name: 'HierarchicalCluster', + parent: 3, + segment: 'analytics', + size: 6714, + }, + { + id: 7, + name: 'MergeEdge', + parent: 3, + segment: 'analytics', + size: 743, + }, + { + id: 8, + name: 'graph', + parent: 2, + segment: 'analytics', + }, + { + id: 9, + name: 'BetweennessCentrality', + parent: 2, + segment: 'analytics', + size: 3534, + }, + { + id: 10, + name: 'LinkDistance', + parent: 2, + segment: 'analytics', + size: 5731, + }, + { + id: 11, + name: 'MaxFlowMinCut', + parent: 2, + segment: 'analytics', + size: 7840, + }, + { + id: 12, + name: 'ShortestPaths', + parent: 2, + segment: 'analytics', + size: 5914, + }, + { + id: 13, + name: 'SpanningTree', + parent: 2, + segment: 'analytics', + size: 3416, + }, + { + id: 14, + name: 'optimization', + parent: 2, + segment: 'analytics', + size: 3500, + }, + { + id: 15, + name: 'AspectRatioBanker', + parent: 14, + segment: 'analytics', + size: 7074, + }, + { + id: 16, + name: 'animate', + parent: 1, + segment: 'animate', + }, + { + id: 17, + name: 'Easing', + parent: 16, + segment: 'animate', + size: 17010, + }, + { + id: 18, + name: 'FunctionSequence', + parent: 16, + segment: 'animate', + size: 5842, + }, + { + id: 19, + name: 'interpolate', + parent: 16, + segment: 'animate', + }, + { + id: 20, + name: 'ArrayInterpolator', + parent: 19, + segment: 'animate', + size: 1983, + }, + { + id: 21, + name: 'ColorInterpolator', + parent: 19, + segment: 'animate', + size: 2047, + }, + { + id: 22, + name: 'DateInterpolator', + parent: 19, + segment: 'animate', + size: 1375, + }, + { + id: 23, + name: 'Interpolator', + parent: 19, + segment: 'animate', + size: 8746, + }, + { + id: 24, + name: 'MatrixInterpolator', + parent: 19, + segment: 'animate', + size: 2202, + }, + { + id: 25, + name: 'NumberInterpolator', + parent: 19, + segment: 'animate', + size: 1382, + }, + { + id: 26, + name: 'ObjectInterpolator', + parent: 19, + segment: 'animate', + size: 1629, + }, + { + id: 27, + name: 'PointInterpolator', + parent: 19, + segment: 'animate', + size: 1675, + }, + { + id: 28, + name: 'RectangleInterpolator', + parent: 19, + segment: 'animate', + size: 2042, + }, + { + id: 29, + name: 'ISchedulable', + parent: 16, + segment: 'animate', + size: 1041, + }, + { + id: 30, + name: 'Parallel', + parent: 16, + segment: 'animate', + size: 5176, + }, + { + id: 31, + name: 'Pause', + parent: 16, + segment: 'animate', + size: 449, + }, + { + id: 32, + name: 'Scheduler', + parent: 16, + segment: 'animate', + size: 5593, + }, + { + id: 33, + name: 'Sequence', + parent: 16, + segment: 'animate', + size: 5534, + }, + { + id: 34, + name: 'Transition', + parent: 16, + segment: 'animate', + size: 9201, + }, + { + id: 35, + name: 'Transitioner', + parent: 16, + segment: 'animate', + size: 19975, + }, + { + id: 36, + name: 'TransitionEvent', + parent: 16, + segment: 'animate', + size: 1116, + }, + { + id: 37, + name: 'Tween', + parent: 16, + segment: 'animate', + size: 6006, + }, + { + id: 38, + name: 'data', + parent: 1, + segment: 'data', + }, + { + id: 39, + name: 'converters', + parent: 38, + segment: 'data', + }, + { + id: 40, + name: 'Converters', + parent: 39, + segment: 'data', + size: 721, + }, + { + id: 41, + name: 'DelimitedTextConverter', + parent: 39, + segment: 'data', + size: 4294, + }, + { + id: 42, + name: 'GraphMLConverter', + parent: 39, + segment: 'data', + size: 9800, + }, + { + id: 43, + name: 'IDataConverter', + parent: 39, + segment: 'data', + size: 1314, + }, + { + id: 44, + name: 'JSONConverter', + parent: 39, + segment: 'data', + size: 2220, + }, + { + id: 45, + name: 'DataField', + parent: 38, + segment: 'data', + size: 1759, + }, + { + id: 46, + name: 'DataSchema', + parent: 38, + segment: 'data', + size: 2165, + }, + { + id: 47, + name: 'DataSet', + parent: 38, + segment: 'data', + size: 586, + }, + { + id: 48, + name: 'DataSource', + parent: 38, + segment: 'data', + size: 3331, + }, + { + id: 49, + name: 'DataTable', + parent: 38, + segment: 'data', + size: 772, + }, + { + id: 50, + name: 'DataUtil', + parent: 38, + segment: 'data', + size: 3322, + }, + { + id: 51, + name: 'display', + parent: 1, + segment: 'display', + }, + { + id: 52, + name: 'DirtySprite', + parent: 51, + segment: 'display', + size: 8833, + }, + { + id: 53, + name: 'LineSprite', + parent: 51, + segment: 'display', + size: 1732, + }, + { + id: 54, + name: 'RectSprite', + parent: 51, + segment: 'display', + size: 3623, + }, + { + id: 55, + name: 'TextSprite', + parent: 51, + segment: 'display', + size: 10066, + }, + { + id: 56, + name: 'flex', + parent: 1, + segment: 'flex', + }, + { + id: 57, + name: 'FlareVis', + parent: 56, + segment: 'flex', + size: 4116, + }, + { + id: 58, + name: 'physics', + parent: 1, + segment: 'physics', + }, + { + id: 59, + name: 'DragForce', + parent: 58, + segment: 'physics', + size: 1082, + }, + { + id: 60, + name: 'GravityForce', + parent: 58, + segment: 'physics', + size: 1336, + }, + { + id: 61, + name: 'IForce', + parent: 58, + segment: 'physics', + size: 319, + }, + { + id: 62, + name: 'NBodyForce', + parent: 58, + segment: 'physics', + size: 10498, + }, + { + id: 63, + name: 'Particle', + parent: 58, + segment: 'physics', + size: 2822, + }, + { + id: 64, + name: 'Simulation', + parent: 58, + segment: 'physics', + size: 9983, + }, +]; diff --git a/src/types/Chart.ts b/src/types/Chart.ts index 02cb50c90..0387c6d73 100644 --- a/src/types/Chart.ts +++ b/src/types/Chart.ts @@ -12,7 +12,19 @@ import { JSXElementConstructor, MutableRefObject, ReactElement, ReactNode } from 'react'; import { GROUP_DATA, INTERACTION_MODE, MARK_ID, SERIES_ID, TRENDLINE_VALUE } from '@constants'; -import { Config, Data, FontWeight, Locale, NumberLocale, Padding, Spec, SymbolShape, TimeLocale, View } from 'vega'; +import { + Config, + Data, + FieldRef, + FontWeight, + Locale, + NumberLocale, + Padding, + Spec, + SymbolShape, + TimeLocale, + View, +} from 'vega'; import { Icon, IconProps } from '@adobe/react-spectrum'; import { IconPropsWithoutChildren } from '@react-spectrum/icon'; @@ -31,6 +43,7 @@ export type BarElement = ReactElement> export type ChartElement = ReactElement>; export type ChartPopoverElement = ReactElement>; export type ChartTooltipElement = ReactElement>; +export type TreemapElement = ReactElement>; export type DonutElement = ReactElement>; export type DonutSummaryElement = ReactElement>; export type LegendElement = ReactElement>; @@ -221,6 +234,42 @@ export interface AreaProps extends MarkProps { metricEnd?: string; } +export interface TreemapProps extends MarkProps { + children?: Children; + /** + * type of color scale that should be used for the points + * use ordinal if the key used for `color` maps to string values ('UT', 'CA', 'NY', etc.) + * use linear if the key used for `color` maps to numeric values (0, 1, 2, etc.) + */ + colorScaleType?: 'linear' | 'ordinal'; + /** Key to to match the the hierarchy of the domains in the scales of the treemap, i.e. the highest parent that isn't root */ + segmentKey?: string; + /** Key in the data that is used to id parent node */ + parent?: FieldRef; + /** Vega color scheme (e.g., 'category10', 'blues') */ + colorScheme?: string; + /** Space between rectangles */ + paddingInner?: number; + /** Space around rectangles */ + paddingOuter?: number; + /** Aspect ratio of the treemap */ + aspectRatio?: number; + /** Color of the borders */ + borderColor?: string; + /** Color of nodes and leaves text */ + textColor?: string; + /** Name of the chart */ + name?: string; + /** Color of the text in the root node */ + rootTextColor?: string; + /** Width of the node borders */ + nodesBorderWidth?: number; + /** Width of the leaf borders */ + leavesBorderWidth?: number; + /** Treemap algorithm */ + layout?: 'squarify' | 'binary' | 'slicedice'; +} + export interface DonutProps extends MarkProps { /** Start angle of the donut in radians (0 is top dead center, and default) */ startAngle?: number; @@ -872,7 +921,8 @@ export type ChartChildElement = | LineElement | ScatterElement | TitleElement - | ComboElement; + | ComboElement + | TreemapElement; export type MarkChildElement = | AnnotationElement | ChartTooltipElement diff --git a/src/types/specBuilderTypes.ts b/src/types/specBuilderTypes.ts index bdf8bb556..91aa374cb 100644 --- a/src/types/specBuilderTypes.ts +++ b/src/types/specBuilderTypes.ts @@ -9,7 +9,7 @@ * OF ANY KIND, either express or implied. See the License for the specific language * governing permissions and limitations under the License. */ -import { Align, Baseline, NumberValue, ScaleType } from 'vega'; +import { Align, Baseline, FieldRef, NumberValue, ScaleType } from 'vega'; import { AnnotationProps, @@ -39,6 +39,7 @@ import { ScatterPathProps, ScatterProps, SegmentLabelProps, + TreemapProps, TrendlineAnnotationProps, TrendlineChildElement, TrendlineProps, @@ -152,6 +153,27 @@ export interface DonutSpecProps extends PartiallyRequired { + children: MarkChildElement[]; + colorScheme: ColorScheme; + segmentKey?: string; + parent?: FieldRef; + paddingInner?: number; + paddingOuter?: number; + aspectRatio?: number; + borderColor?: string; + name?: string; + textColor?: string; + rootTextColor?: string; + nodesBorderWidth?: number; + leavesBorderWidth?: number; + highlightedItem?: HighlightedItem; + idKey: string; + index: number; +} + export interface DonutSummarySpecProps extends PartiallyRequired { donutProps: DonutSpecProps; } diff --git a/src/utils/utils.ts b/src/utils/utils.ts index f6636281c..76a3220b8 100644 --- a/src/utils/utils.ts +++ b/src/utils/utils.ts @@ -30,7 +30,7 @@ import { Trendline, TrendlineAnnotation, } from '@rsc'; -import { Combo } from '@rsc/alpha'; +import { Combo, Treemap } from '@rsc/alpha'; import { BigNumber, Donut, DonutSummary, SegmentLabel } from '@rsc/rc'; import { View } from 'vega'; @@ -54,6 +54,7 @@ import { MarkChildElement, RscElement, ScatterElement, + TreemapElement, TrendlineElement, } from '../types'; @@ -68,6 +69,7 @@ type ElementCounts = { line: number; scatter: number; combo: number; + treemap: number; }; // coerces a value that could be a single value or an array of that value to an array @@ -102,6 +104,7 @@ export const sanitizeRscChartChildren = (children: unknown): ChartChildElement[] Scatter.displayName, Title.displayName, Combo.displayName, + Treemap.displayName, ] as string[]; return toArray(children) .flat() @@ -353,6 +356,9 @@ const getElementName = (element: unknown, elementCounts: ElementCounts) => { case Combo.displayName: elementCounts.combo++; return getComponentName(element as ComboElement, `combo${elementCounts.combo}`); + case Treemap.displayName: + elementCounts.treemap++; + return getComponentName(element as TreemapElement, `treemap${elementCounts.treemap}`); default: return ''; } @@ -381,6 +387,7 @@ const initElementCounts = (): ElementCounts => ({ line: -1, scatter: -1, combo: -1, + treemap: -1, }); /**