@memberjunction/ng-word-cloud 6.1.4 → 6.2.0-edge.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,5 @@
1
1
  import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, } from '@angular/core';
2
- import { computeWordCloudLayout } from './word-cloud.layout';
2
+ import { ComputeWordCloudLayout } from './word-cloud.layout';
3
3
  import * as i0 from "@angular/core";
4
4
  const _forTrack0 = ($index, $item) => $item.Text;
5
5
  function MJWordCloudComponent_For_2_Template(rf, ctx) { if (rf & 1) {
@@ -109,7 +109,7 @@ export class MJWordCloudComponent {
109
109
  Layout: this.Layout,
110
110
  MaxItems: this.MaxItems,
111
111
  };
112
- const result = computeWordCloudLayout(this.Items, config);
112
+ const result = ComputeWordCloudLayout(this.Items, config);
113
113
  this.ViewBox = result.ViewBox;
114
114
  this.LayoutItems = this.applyColors(result.Items);
115
115
  }
@@ -1 +1 @@
1
- {"version":3,"file":"word-cloud.component.js","sourceRoot":"","sources":["../../src/lib/word-cloud.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n OnChanges,\n SimpleChanges,\n ChangeDetectionStrategy,\n} from '@angular/core';\nimport { WordCloudItem, WordCloudItemEvent, WordCloudLayoutItem } from './word-cloud.types';\nimport { computeWordCloudLayout, WordCloudLayoutConfig } from './word-cloud.layout';\n\n/** Fixed palette for categorical color mode, using MJ design-token-friendly CSS values. */\nconst CATEGORICAL_PALETTE: string[] = [\n 'var(--mj-brand-primary)',\n 'var(--mj-status-success)',\n 'var(--mj-status-warning)',\n 'var(--mj-status-error)',\n 'var(--mj-status-info)',\n 'var(--mj-brand-primary-hover)',\n 'var(--mj-status-success-text)',\n 'var(--mj-status-warning-text)',\n];\n\n/**\n * A reusable, SVG-based word cloud component.\n *\n * Renders weighted text items in a spiral or rectangular layout with\n * collision detection. Supports multiple color modes using MJ design tokens,\n * optional entry animation, and click/hover interaction.\n *\n * @example\n * ```html\n * <mj-word-cloud\n * [Items]=\"words\"\n * [MaxFontSize]=\"56\"\n * ColorMode=\"categorical\"\n * (ItemClick)=\"onWordClick($event)\">\n * </mj-word-cloud>\n * ```\n */\n@Component({\n selector: 'mj-word-cloud',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <svg\n [attr.viewBox]=\"ViewBox\"\n class=\"mj-word-cloud\"\n [class.mj-word-cloud-animated]=\"Animate\"\n preserveAspectRatio=\"xMidYMid meet\">\n @for (item of LayoutItems; track item.Text) {\n <text\n [attr.x]=\"item.X\"\n [attr.y]=\"item.Y\"\n [attr.font-size]=\"item.FontSize + 'px'\"\n [attr.transform]=\"'rotate(' + item.Rotation + ' ' + item.X + ' ' + item.Y + ')'\"\n [style.fill]=\"item.Color\"\n [style.opacity]=\"item.Opacity\"\n [style.animation-delay]=\"Animate ? (item.Index * 30) + 'ms' : '0ms'\"\n class=\"mj-word-cloud-item\"\n [class.mj-word-cloud-interactive]=\"Interactive\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n (click)=\"OnItemClick(item, $event)\"\n (mouseenter)=\"OnItemHover(item, $event)\"\n (mouseleave)=\"OnItemLeave(item, $event)\">\n {{ item.Text }}\n </text>\n }\n </svg>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .mj-word-cloud {\n width: 100%;\n height: 100%;\n }\n\n .mj-word-cloud-item {\n font-family: var(--mj-font-family, inherit);\n transition: opacity 0.2s ease, filter 0.2s ease;\n user-select: none;\n }\n\n .mj-word-cloud-interactive {\n cursor: pointer;\n }\n\n .mj-word-cloud-interactive:hover {\n opacity: 0.8 !important;\n filter: brightness(1.2);\n }\n\n .mj-word-cloud-animated .mj-word-cloud-item {\n animation: mj-word-cloud-fade-in 0.4s ease-out both;\n }\n\n @keyframes mj-word-cloud-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n `],\n})\nexport class MJWordCloudComponent implements OnChanges {\n // --------------- Inputs ---------------\n\n /** Data items to display in the cloud. */\n @Input() Items: WordCloudItem[] = [];\n\n /** Minimum font size in pixels for the lowest-weighted item. */\n @Input() MinFontSize = 12;\n\n /** Maximum font size in pixels for the highest-weighted item. */\n @Input() MaxFontSize = 48;\n\n /** Layout algorithm used for word placement. */\n @Input() Layout: 'spiral' | 'rectangular' = 'spiral';\n\n /**\n * How colors are assigned to words.\n * - `brand`: all words use --mj-brand-primary with opacity based on weight.\n * - `categorical`: distinct colors from a fixed palette, assigned by Category.\n * - `weight-gradient`: interpolates from muted to brand-primary based on weight.\n */\n @Input() ColorMode: 'brand' | 'categorical' | 'weight-gradient' = 'brand';\n\n /** Whether words are clickable / hoverable. */\n @Input() Interactive = true;\n\n /** Maximum number of items to display (heaviest are kept). */\n @Input() MaxItems = 100;\n\n /** Whether to animate words in with a staggered fade. */\n @Input() Animate = true;\n\n // --------------- Outputs ---------------\n\n /** Emitted when a word is clicked. */\n @Output() ItemClick = new EventEmitter<WordCloudItemEvent>();\n\n /** Emitted when the pointer enters a word. */\n @Output() ItemHover = new EventEmitter<WordCloudItemEvent>();\n\n /** Emitted when the pointer leaves a word. */\n @Output() ItemLeave = new EventEmitter<WordCloudItemEvent>();\n\n // --------------- Public computed state ---------------\n\n /** Positioned items ready for rendering. */\n LayoutItems: WordCloudLayoutItem[] = [];\n\n /** SVG viewBox attribute value. */\n ViewBox = '0 0 100 100';\n\n // --------------- Lifecycle ---------------\n\n ngOnChanges(_changes: SimpleChanges): void {\n this.recomputeLayout();\n }\n\n // --------------- Event handlers ---------------\n\n OnItemClick(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemClick.emit({ Item: item, Event: event });\n }\n }\n\n OnItemHover(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemHover.emit({ Item: item, Event: event });\n }\n }\n\n OnItemLeave(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemLeave.emit({ Item: item, Event: event });\n }\n }\n\n // --------------- Private helpers ---------------\n\n private recomputeLayout(): void {\n const config: WordCloudLayoutConfig = {\n MinFontSize: this.MinFontSize,\n MaxFontSize: this.MaxFontSize,\n Layout: this.Layout,\n MaxItems: this.MaxItems,\n };\n\n const result = computeWordCloudLayout(this.Items, config);\n this.ViewBox = result.ViewBox;\n this.LayoutItems = this.applyColors(result.Items);\n }\n\n /**\n * Applies color and opacity to each layout item based on the current ColorMode.\n */\n private applyColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n switch (this.ColorMode) {\n case 'brand':\n return this.applyBrandColors(items);\n case 'categorical':\n return this.applyCategoricalColors(items);\n case 'weight-gradient':\n return this.applyWeightGradientColors(items);\n default:\n return items;\n }\n }\n\n /**\n * Brand mode: all words use --mj-brand-primary; opacity varies with weight.\n */\n private applyBrandColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n return items.map(item => ({\n ...item,\n Color: 'var(--mj-brand-primary)',\n Opacity: 0.4 + item.Weight * 0.6, // range 0.4 - 1.0\n }));\n }\n\n /**\n * Categorical mode: words with the same Category share a color from the palette.\n */\n private applyCategoricalColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n const categoryIndex = new Map<string, number>();\n let nextIndex = 0;\n\n return items.map(item => {\n const key = item.Category ?? item.Text;\n if (!categoryIndex.has(key)) {\n categoryIndex.set(key, nextIndex);\n nextIndex++;\n }\n const colorIdx = categoryIndex.get(key)! % CATEGORICAL_PALETTE.length;\n return {\n ...item,\n Color: CATEGORICAL_PALETTE[colorIdx],\n Opacity: 1,\n };\n });\n }\n\n /**\n * Weight-gradient mode: low-weight words use --mj-text-muted, high-weight\n * words use --mj-brand-primary, interpolated via color-mix().\n */\n private applyWeightGradientColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n return items.map(item => {\n const pct = Math.round(item.Weight * 100);\n return {\n ...item,\n Color: `color-mix(in srgb, var(--mj-brand-primary) ${pct}%, var(--mj-text-muted))`,\n Opacity: 1,\n };\n });\n }\n}\n"]}
1
+ {"version":3,"file":"word-cloud.component.js","sourceRoot":"","sources":["../../src/lib/word-cloud.component.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n Component,\n Input,\n Output,\n EventEmitter,\n OnChanges,\n SimpleChanges,\n ChangeDetectionStrategy,\n} from '@angular/core';\nimport { WordCloudItem, WordCloudItemEvent, WordCloudLayoutItem } from './word-cloud.types';\nimport { ComputeWordCloudLayout, WordCloudLayoutConfig } from './word-cloud.layout';\n\n/** Fixed palette for categorical color mode, using MJ design-token-friendly CSS values. */\nconst CATEGORICAL_PALETTE: string[] = [\n 'var(--mj-brand-primary)',\n 'var(--mj-status-success)',\n 'var(--mj-status-warning)',\n 'var(--mj-status-error)',\n 'var(--mj-status-info)',\n 'var(--mj-brand-primary-hover)',\n 'var(--mj-status-success-text)',\n 'var(--mj-status-warning-text)',\n];\n\n/**\n * A reusable, SVG-based word cloud component.\n *\n * Renders weighted text items in a spiral or rectangular layout with\n * collision detection. Supports multiple color modes using MJ design tokens,\n * optional entry animation, and click/hover interaction.\n *\n * @example\n * ```html\n * <mj-word-cloud\n * [Items]=\"words\"\n * [MaxFontSize]=\"56\"\n * ColorMode=\"categorical\"\n * (ItemClick)=\"onWordClick($event)\">\n * </mj-word-cloud>\n * ```\n */\n@Component({\n selector: 'mj-word-cloud',\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n template: `\n <svg\n [attr.viewBox]=\"ViewBox\"\n class=\"mj-word-cloud\"\n [class.mj-word-cloud-animated]=\"Animate\"\n preserveAspectRatio=\"xMidYMid meet\">\n @for (item of LayoutItems; track item.Text) {\n <text\n [attr.x]=\"item.X\"\n [attr.y]=\"item.Y\"\n [attr.font-size]=\"item.FontSize + 'px'\"\n [attr.transform]=\"'rotate(' + item.Rotation + ' ' + item.X + ' ' + item.Y + ')'\"\n [style.fill]=\"item.Color\"\n [style.opacity]=\"item.Opacity\"\n [style.animation-delay]=\"Animate ? (item.Index * 30) + 'ms' : '0ms'\"\n class=\"mj-word-cloud-item\"\n [class.mj-word-cloud-interactive]=\"Interactive\"\n text-anchor=\"middle\"\n dominant-baseline=\"central\"\n (click)=\"OnItemClick(item, $event)\"\n (mouseenter)=\"OnItemHover(item, $event)\"\n (mouseleave)=\"OnItemLeave(item, $event)\">\n {{ item.Text }}\n </text>\n }\n </svg>\n `,\n styles: [`\n :host {\n display: block;\n width: 100%;\n height: 100%;\n }\n\n .mj-word-cloud {\n width: 100%;\n height: 100%;\n }\n\n .mj-word-cloud-item {\n font-family: var(--mj-font-family, inherit);\n transition: opacity 0.2s ease, filter 0.2s ease;\n user-select: none;\n }\n\n .mj-word-cloud-interactive {\n cursor: pointer;\n }\n\n .mj-word-cloud-interactive:hover {\n opacity: 0.8 !important;\n filter: brightness(1.2);\n }\n\n .mj-word-cloud-animated .mj-word-cloud-item {\n animation: mj-word-cloud-fade-in 0.4s ease-out both;\n }\n\n @keyframes mj-word-cloud-fade-in {\n from {\n opacity: 0;\n }\n to {\n opacity: 1;\n }\n }\n `],\n})\nexport class MJWordCloudComponent implements OnChanges {\n // --------------- Inputs ---------------\n\n /** Data items to display in the cloud. */\n @Input() Items: WordCloudItem[] = [];\n\n /** Minimum font size in pixels for the lowest-weighted item. */\n @Input() MinFontSize = 12;\n\n /** Maximum font size in pixels for the highest-weighted item. */\n @Input() MaxFontSize = 48;\n\n /** Layout algorithm used for word placement. */\n @Input() Layout: 'spiral' | 'rectangular' = 'spiral';\n\n /**\n * How colors are assigned to words.\n * - `brand`: all words use --mj-brand-primary with opacity based on weight.\n * - `categorical`: distinct colors from a fixed palette, assigned by Category.\n * - `weight-gradient`: interpolates from muted to brand-primary based on weight.\n */\n @Input() ColorMode: 'brand' | 'categorical' | 'weight-gradient' = 'brand';\n\n /** Whether words are clickable / hoverable. */\n @Input() Interactive = true;\n\n /** Maximum number of items to display (heaviest are kept). */\n @Input() MaxItems = 100;\n\n /** Whether to animate words in with a staggered fade. */\n @Input() Animate = true;\n\n // --------------- Outputs ---------------\n\n /** Emitted when a word is clicked. */\n @Output() ItemClick = new EventEmitter<WordCloudItemEvent>();\n\n /** Emitted when the pointer enters a word. */\n @Output() ItemHover = new EventEmitter<WordCloudItemEvent>();\n\n /** Emitted when the pointer leaves a word. */\n @Output() ItemLeave = new EventEmitter<WordCloudItemEvent>();\n\n // --------------- Public computed state ---------------\n\n /** Positioned items ready for rendering. */\n LayoutItems: WordCloudLayoutItem[] = [];\n\n /** SVG viewBox attribute value. */\n ViewBox = '0 0 100 100';\n\n // --------------- Lifecycle ---------------\n\n ngOnChanges(_changes: SimpleChanges): void {\n this.recomputeLayout();\n }\n\n // --------------- Event handlers ---------------\n\n OnItemClick(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemClick.emit({ Item: item, Event: event });\n }\n }\n\n OnItemHover(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemHover.emit({ Item: item, Event: event });\n }\n }\n\n OnItemLeave(item: WordCloudLayoutItem, event: MouseEvent): void {\n if (this.Interactive) {\n this.ItemLeave.emit({ Item: item, Event: event });\n }\n }\n\n // --------------- Private helpers ---------------\n\n private recomputeLayout(): void {\n const config: WordCloudLayoutConfig = {\n MinFontSize: this.MinFontSize,\n MaxFontSize: this.MaxFontSize,\n Layout: this.Layout,\n MaxItems: this.MaxItems,\n };\n\n const result = ComputeWordCloudLayout(this.Items, config);\n this.ViewBox = result.ViewBox;\n this.LayoutItems = this.applyColors(result.Items);\n }\n\n /**\n * Applies color and opacity to each layout item based on the current ColorMode.\n */\n private applyColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n switch (this.ColorMode) {\n case 'brand':\n return this.applyBrandColors(items);\n case 'categorical':\n return this.applyCategoricalColors(items);\n case 'weight-gradient':\n return this.applyWeightGradientColors(items);\n default:\n return items;\n }\n }\n\n /**\n * Brand mode: all words use --mj-brand-primary; opacity varies with weight.\n */\n private applyBrandColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n return items.map(item => ({\n ...item,\n Color: 'var(--mj-brand-primary)',\n Opacity: 0.4 + item.Weight * 0.6, // range 0.4 - 1.0\n }));\n }\n\n /**\n * Categorical mode: words with the same Category share a color from the palette.\n */\n private applyCategoricalColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n const categoryIndex = new Map<string, number>();\n let nextIndex = 0;\n\n return items.map(item => {\n const key = item.Category ?? item.Text;\n if (!categoryIndex.has(key)) {\n categoryIndex.set(key, nextIndex);\n nextIndex++;\n }\n const colorIdx = categoryIndex.get(key)! % CATEGORICAL_PALETTE.length;\n return {\n ...item,\n Color: CATEGORICAL_PALETTE[colorIdx],\n Opacity: 1,\n };\n });\n }\n\n /**\n * Weight-gradient mode: low-weight words use --mj-text-muted, high-weight\n * words use --mj-brand-primary, interpolated via color-mix().\n */\n private applyWeightGradientColors(items: WordCloudLayoutItem[]): WordCloudLayoutItem[] {\n return items.map(item => {\n const pct = Math.round(item.Weight * 100);\n return {\n ...item,\n Color: `color-mix(in srgb, var(--mj-brand-primary) ${pct}%, var(--mj-text-muted))`,\n Opacity: 1,\n };\n });\n }\n}\n"]}
@@ -12,6 +12,11 @@ export interface WordCloudLayoutConfig {
12
12
  * Computes the layout for all word cloud items, returning positioned items
13
13
  * and the overall bounding viewBox.
14
14
  */
15
+ export declare function ComputeWordCloudLayout(items: WordCloudItem[], config: WordCloudLayoutConfig): {
16
+ Items: WordCloudLayoutItem[];
17
+ ViewBox: string;
18
+ };
19
+ /** @deprecated Use {@link ComputeWordCloudLayout}. */
15
20
  export declare function computeWordCloudLayout(items: WordCloudItem[], config: WordCloudLayoutConfig): {
16
21
  Items: WordCloudLayoutItem[];
17
22
  ViewBox: string;
@@ -1 +1 @@
1
- {"version":3,"file":"word-cloud.layout.d.ts","sourceRoot":"","sources":["../../src/lib/word-cloud.layout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,aAAa,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkID;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,aAAa,EAAE,EACtB,MAAM,EAAE,qBAAqB,GAC5B;IAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA2DnD"}
1
+ {"version":3,"file":"word-cloud.layout.d.ts","sourceRoot":"","sources":["../../src/lib/word-cloud.layout.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAExE;;GAEG;AACH,MAAM,WAAW,qBAAqB;IACpC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,QAAQ,GAAG,aAAa,CAAC;IACjC,QAAQ,EAAE,MAAM,CAAC;CAClB;AAkID;;;GAGG;AACH,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,aAAa,EAAE,EACtB,MAAM,EAAE,qBAAqB,GAC5B;IAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CA2DnD;AAED,sDAAsD;AACtD,wBAAgB,sBAAsB,CACpC,KAAK,EAAE,aAAa,EAAE,EACtB,MAAM,EAAE,qBAAqB,GAC5B;IAAE,KAAK,EAAE,mBAAmB,EAAE,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAEnD"}
@@ -102,7 +102,7 @@ function shouldRotate(index) {
102
102
  * Computes the layout for all word cloud items, returning positioned items
103
103
  * and the overall bounding viewBox.
104
104
  */
105
- export function computeWordCloudLayout(items, config) {
105
+ export function ComputeWordCloudLayout(items, config) {
106
106
  if (!items || items.length === 0) {
107
107
  return { Items: [], ViewBox: '0 0 100 100' };
108
108
  }
@@ -150,6 +150,10 @@ export function computeWordCloudLayout(items, config) {
150
150
  const viewBox = computeViewBox(placedBoxes);
151
151
  return { Items: layoutItems, ViewBox: viewBox };
152
152
  }
153
+ /** @deprecated Use {@link ComputeWordCloudLayout}. */
154
+ export function computeWordCloudLayout(items, config) {
155
+ return ComputeWordCloudLayout(items, config);
156
+ }
153
157
  /**
154
158
  * Computes an SVG viewBox string that fits all placed bounding boxes
155
159
  * with a small amount of padding.
@@ -1 +1 @@
1
- {"version":3,"file":"word-cloud.layout.js","sourceRoot":"","sources":["../../src/lib/word-cloud.layout.ts"],"names":[],"mappings":"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","sourcesContent":["import { WordCloudItem, WordCloudLayoutItem } from './word-cloud.types';\n\n/**\n * Configuration for the layout engine.\n */\nexport interface WordCloudLayoutConfig {\n MinFontSize: number;\n MaxFontSize: number;\n Layout: 'spiral' | 'rectangular';\n MaxItems: number;\n}\n\n/**\n * Axis-aligned bounding box for collision detection.\n */\ninterface BoundingBox {\n Left: number;\n Top: number;\n Right: number;\n Bottom: number;\n}\n\n/**\n * Estimates the width of a text string at a given font size.\n * Uses an average character width ratio since we cannot measure DOM elements\n * in a pure layout function.\n */\nfunction estimateTextWidth(text: string, fontSize: number): number {\n // Average character width is roughly 0.6x the font size for sans-serif fonts\n const avgCharWidthRatio = 0.6;\n return text.length * fontSize * avgCharWidthRatio;\n}\n\n/**\n * Creates a bounding box for a word at a given position and font size,\n * accounting for rotation.\n */\nfunction createBoundingBox(\n x: number,\n y: number,\n text: string,\n fontSize: number,\n rotation: number\n): BoundingBox {\n const textWidth = estimateTextWidth(text, fontSize);\n const textHeight = fontSize * 1.2; // line-height factor\n const padding = fontSize * 0.15; // small padding between words\n\n // For 90-degree rotation, width and height swap\n const effectiveWidth = rotation === 90 ? textHeight : textWidth;\n const effectiveHeight = rotation === 90 ? textWidth : textHeight;\n\n return {\n Left: x - effectiveWidth / 2 - padding,\n Top: y - effectiveHeight / 2 - padding,\n Right: x + effectiveWidth / 2 + padding,\n Bottom: y + effectiveHeight / 2 + padding,\n };\n}\n\n/**\n * Checks whether two bounding boxes overlap.\n */\nfunction boxesOverlap(a: BoundingBox, b: BoundingBox): boolean {\n return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;\n}\n\n/**\n * Checks whether a candidate bounding box collides with any of the placed boxes.\n */\nfunction hasCollision(candidate: BoundingBox, placed: BoundingBox[]): boolean {\n for (const box of placed) {\n if (boxesOverlap(candidate, box)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Generates (x, y) positions along an Archimedean spiral from the center.\n */\nfunction* spiralPositions(centerX: number, centerY: number, maxAttempts: number): Generator<{ X: number; Y: number }> {\n const spiralStep = 1; // angular step in radians\n const radiusGrowth = 2; // pixels per radian\n\n for (let i = 0; i < maxAttempts; i++) {\n const angle = spiralStep * i * 0.1;\n const radius = radiusGrowth * angle;\n yield {\n X: centerX + radius * Math.cos(angle),\n Y: centerY + radius * Math.sin(angle),\n };\n }\n}\n\n/**\n * Generates positions in a rectangular grid expanding from the center.\n */\nfunction* rectangularPositions(centerX: number, centerY: number, maxAttempts: number): Generator<{ X: number; Y: number }> {\n const step = 8;\n yield { X: centerX, Y: centerY };\n let attempt = 1;\n\n for (let ring = 1; attempt < maxAttempts; ring++) {\n const offset = ring * step;\n // Top and bottom edges\n for (let dx = -offset; dx <= offset && attempt < maxAttempts; dx += step) {\n yield { X: centerX + dx, Y: centerY - offset };\n attempt++;\n yield { X: centerX + dx, Y: centerY + offset };\n attempt++;\n }\n // Left and right edges (excluding corners)\n for (let dy = -offset + step; dy < offset && attempt < maxAttempts; dy += step) {\n yield { X: centerX - offset, Y: centerY + dy };\n attempt++;\n yield { X: centerX + offset, Y: centerY + dy };\n attempt++;\n }\n }\n}\n\n/**\n * Maps a weight (0-1) to a font size between min and max.\n */\nfunction weightToFontSize(weight: number, minFont: number, maxFont: number): number {\n const clamped = Math.max(0, Math.min(1, weight));\n return minFont + clamped * (maxFont - minFont);\n}\n\n/**\n * Decides whether a word should be rotated (90 degrees) based on its index.\n * Roughly 20% of words are rotated for visual variety.\n */\nfunction shouldRotate(index: number): boolean {\n // Deterministic pseudo-random based on index\n return index % 5 === 3;\n}\n\n/**\n * Computes the layout for all word cloud items, returning positioned items\n * and the overall bounding viewBox.\n */\nexport function computeWordCloudLayout(\n items: WordCloudItem[],\n config: WordCloudLayoutConfig\n): { Items: WordCloudLayoutItem[]; ViewBox: string } {\n if (!items || items.length === 0) {\n return { Items: [], ViewBox: '0 0 100 100' };\n }\n\n // Take top N items by weight\n const sorted = [...items]\n .sort((a, b) => b.Weight - a.Weight)\n .slice(0, config.MaxItems);\n\n const placedBoxes: BoundingBox[] = [];\n const layoutItems: WordCloudLayoutItem[] = [];\n const maxAttempts = 500;\n\n const centerX = 0;\n const centerY = 0;\n\n for (let i = 0; i < sorted.length; i++) {\n const item = sorted[i];\n const fontSize = weightToFontSize(item.Weight, config.MinFontSize, config.MaxFontSize);\n const rotation = shouldRotate(i) ? 90 : 0;\n\n const positionGenerator =\n config.Layout === 'rectangular'\n ? rectangularPositions(centerX, centerY, maxAttempts)\n : spiralPositions(centerX, centerY, maxAttempts);\n\n let placed = false;\n\n for (const pos of positionGenerator) {\n const box = createBoundingBox(pos.X, pos.Y, item.Text, fontSize, rotation);\n\n if (!hasCollision(box, placedBoxes)) {\n placedBoxes.push(box);\n layoutItems.push({\n ...item,\n FontSize: fontSize,\n X: pos.X,\n Y: pos.Y,\n Rotation: rotation,\n Color: '', // assigned by the component based on ColorMode\n Opacity: 1,\n Index: layoutItems.length,\n });\n placed = true;\n break;\n }\n }\n\n // If no position found after maxAttempts, skip the word\n if (!placed) {\n continue;\n }\n }\n\n // Compute the overall bounding box for the viewBox\n const viewBox = computeViewBox(placedBoxes);\n\n return { Items: layoutItems, ViewBox: viewBox };\n}\n\n/**\n * Computes an SVG viewBox string that fits all placed bounding boxes\n * with a small amount of padding.\n */\nfunction computeViewBox(boxes: BoundingBox[]): string {\n if (boxes.length === 0) {\n return '0 0 100 100';\n }\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const box of boxes) {\n if (box.Left < minX) minX = box.Left;\n if (box.Top < minY) minY = box.Top;\n if (box.Right > maxX) maxX = box.Right;\n if (box.Bottom > maxY) maxY = box.Bottom;\n }\n\n const padding = 20;\n const x = minX - padding;\n const y = minY - padding;\n const width = maxX - minX + padding * 2;\n const height = maxY - minY + padding * 2;\n\n return `${x} ${y} ${width} ${height}`;\n}\n"]}
1
+ {"version":3,"file":"word-cloud.layout.js","sourceRoot":"","sources":["../../src/lib/word-cloud.layout.ts"],"names":[],"mappings":"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","sourcesContent":["import { WordCloudItem, WordCloudLayoutItem } from './word-cloud.types';\n\n/**\n * Configuration for the layout engine.\n */\nexport interface WordCloudLayoutConfig {\n MinFontSize: number;\n MaxFontSize: number;\n Layout: 'spiral' | 'rectangular';\n MaxItems: number;\n}\n\n/**\n * Axis-aligned bounding box for collision detection.\n */\ninterface BoundingBox {\n Left: number;\n Top: number;\n Right: number;\n Bottom: number;\n}\n\n/**\n * Estimates the width of a text string at a given font size.\n * Uses an average character width ratio since we cannot measure DOM elements\n * in a pure layout function.\n */\nfunction estimateTextWidth(text: string, fontSize: number): number {\n // Average character width is roughly 0.6x the font size for sans-serif fonts\n const avgCharWidthRatio = 0.6;\n return text.length * fontSize * avgCharWidthRatio;\n}\n\n/**\n * Creates a bounding box for a word at a given position and font size,\n * accounting for rotation.\n */\nfunction createBoundingBox(\n x: number,\n y: number,\n text: string,\n fontSize: number,\n rotation: number\n): BoundingBox {\n const textWidth = estimateTextWidth(text, fontSize);\n const textHeight = fontSize * 1.2; // line-height factor\n const padding = fontSize * 0.15; // small padding between words\n\n // For 90-degree rotation, width and height swap\n const effectiveWidth = rotation === 90 ? textHeight : textWidth;\n const effectiveHeight = rotation === 90 ? textWidth : textHeight;\n\n return {\n Left: x - effectiveWidth / 2 - padding,\n Top: y - effectiveHeight / 2 - padding,\n Right: x + effectiveWidth / 2 + padding,\n Bottom: y + effectiveHeight / 2 + padding,\n };\n}\n\n/**\n * Checks whether two bounding boxes overlap.\n */\nfunction boxesOverlap(a: BoundingBox, b: BoundingBox): boolean {\n return a.Left < b.Right && a.Right > b.Left && a.Top < b.Bottom && a.Bottom > b.Top;\n}\n\n/**\n * Checks whether a candidate bounding box collides with any of the placed boxes.\n */\nfunction hasCollision(candidate: BoundingBox, placed: BoundingBox[]): boolean {\n for (const box of placed) {\n if (boxesOverlap(candidate, box)) {\n return true;\n }\n }\n return false;\n}\n\n/**\n * Generates (x, y) positions along an Archimedean spiral from the center.\n */\nfunction* spiralPositions(centerX: number, centerY: number, maxAttempts: number): Generator<{ X: number; Y: number }> {\n const spiralStep = 1; // angular step in radians\n const radiusGrowth = 2; // pixels per radian\n\n for (let i = 0; i < maxAttempts; i++) {\n const angle = spiralStep * i * 0.1;\n const radius = radiusGrowth * angle;\n yield {\n X: centerX + radius * Math.cos(angle),\n Y: centerY + radius * Math.sin(angle),\n };\n }\n}\n\n/**\n * Generates positions in a rectangular grid expanding from the center.\n */\nfunction* rectangularPositions(centerX: number, centerY: number, maxAttempts: number): Generator<{ X: number; Y: number }> {\n const step = 8;\n yield { X: centerX, Y: centerY };\n let attempt = 1;\n\n for (let ring = 1; attempt < maxAttempts; ring++) {\n const offset = ring * step;\n // Top and bottom edges\n for (let dx = -offset; dx <= offset && attempt < maxAttempts; dx += step) {\n yield { X: centerX + dx, Y: centerY - offset };\n attempt++;\n yield { X: centerX + dx, Y: centerY + offset };\n attempt++;\n }\n // Left and right edges (excluding corners)\n for (let dy = -offset + step; dy < offset && attempt < maxAttempts; dy += step) {\n yield { X: centerX - offset, Y: centerY + dy };\n attempt++;\n yield { X: centerX + offset, Y: centerY + dy };\n attempt++;\n }\n }\n}\n\n/**\n * Maps a weight (0-1) to a font size between min and max.\n */\nfunction weightToFontSize(weight: number, minFont: number, maxFont: number): number {\n const clamped = Math.max(0, Math.min(1, weight));\n return minFont + clamped * (maxFont - minFont);\n}\n\n/**\n * Decides whether a word should be rotated (90 degrees) based on its index.\n * Roughly 20% of words are rotated for visual variety.\n */\nfunction shouldRotate(index: number): boolean {\n // Deterministic pseudo-random based on index\n return index % 5 === 3;\n}\n\n/**\n * Computes the layout for all word cloud items, returning positioned items\n * and the overall bounding viewBox.\n */\nexport function ComputeWordCloudLayout(\n items: WordCloudItem[],\n config: WordCloudLayoutConfig\n): { Items: WordCloudLayoutItem[]; ViewBox: string } {\n if (!items || items.length === 0) {\n return { Items: [], ViewBox: '0 0 100 100' };\n }\n\n // Take top N items by weight\n const sorted = [...items]\n .sort((a, b) => b.Weight - a.Weight)\n .slice(0, config.MaxItems);\n\n const placedBoxes: BoundingBox[] = [];\n const layoutItems: WordCloudLayoutItem[] = [];\n const maxAttempts = 500;\n\n const centerX = 0;\n const centerY = 0;\n\n for (let i = 0; i < sorted.length; i++) {\n const item = sorted[i];\n const fontSize = weightToFontSize(item.Weight, config.MinFontSize, config.MaxFontSize);\n const rotation = shouldRotate(i) ? 90 : 0;\n\n const positionGenerator =\n config.Layout === 'rectangular'\n ? rectangularPositions(centerX, centerY, maxAttempts)\n : spiralPositions(centerX, centerY, maxAttempts);\n\n let placed = false;\n\n for (const pos of positionGenerator) {\n const box = createBoundingBox(pos.X, pos.Y, item.Text, fontSize, rotation);\n\n if (!hasCollision(box, placedBoxes)) {\n placedBoxes.push(box);\n layoutItems.push({\n ...item,\n FontSize: fontSize,\n X: pos.X,\n Y: pos.Y,\n Rotation: rotation,\n Color: '', // assigned by the component based on ColorMode\n Opacity: 1,\n Index: layoutItems.length,\n });\n placed = true;\n break;\n }\n }\n\n // If no position found after maxAttempts, skip the word\n if (!placed) {\n continue;\n }\n }\n\n // Compute the overall bounding box for the viewBox\n const viewBox = computeViewBox(placedBoxes);\n\n return { Items: layoutItems, ViewBox: viewBox };\n}\n\n/** @deprecated Use {@link ComputeWordCloudLayout}. */\nexport function computeWordCloudLayout(\n items: WordCloudItem[],\n config: WordCloudLayoutConfig\n): { Items: WordCloudLayoutItem[]; ViewBox: string } {\n return ComputeWordCloudLayout(items, config);\n}\n\n/**\n * Computes an SVG viewBox string that fits all placed bounding boxes\n * with a small amount of padding.\n */\nfunction computeViewBox(boxes: BoundingBox[]): string {\n if (boxes.length === 0) {\n return '0 0 100 100';\n }\n\n let minX = Infinity;\n let minY = Infinity;\n let maxX = -Infinity;\n let maxY = -Infinity;\n\n for (const box of boxes) {\n if (box.Left < minX) minX = box.Left;\n if (box.Top < minY) minY = box.Top;\n if (box.Right > maxX) maxX = box.Right;\n if (box.Bottom > maxY) maxY = box.Bottom;\n }\n\n const padding = 20;\n const x = minX - padding;\n const y = minY - padding;\n const width = maxX - minX + padding * 2;\n const height = maxY - minY + padding * 2;\n\n return `${x} ${y} ${width} ${height}`;\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-word-cloud",
3
- "version": "6.1.4",
3
+ "version": "6.2.0-edge.1",
4
4
  "description": "MemberJunction: Reusable SVG-based word cloud component for Angular.",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -17,7 +17,7 @@
17
17
  "devDependencies": {
18
18
  "@angular/compiler": "21.2.22",
19
19
  "@angular/compiler-cli": "21.2.22",
20
- "@memberjunction/ng-test-utils": "6.1.4",
20
+ "@memberjunction/ng-test-utils": "6.2.0-edge.1",
21
21
  "vitest": "^4.0.18"
22
22
  },
23
23
  "peerDependencies": {