mirror of
https://github.com/apache/superset.git
synced 2026-04-20 08:34:37 +00:00
fix(deps): remove encodable dependency and pin query-string to fix Dependabot CI failures (#37450)
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -18,18 +18,15 @@
|
||||
*/
|
||||
import { PureComponent } from 'react';
|
||||
import cloudLayout from 'd3-cloud';
|
||||
import {
|
||||
PlainObject,
|
||||
createEncoderFactory,
|
||||
DeriveEncoding,
|
||||
Encoder,
|
||||
} from 'encodable';
|
||||
import { scaleLinear } from 'd3-scale';
|
||||
import { seed, CategoricalColorNamespace } from '@superset-ui/core';
|
||||
import { SupersetTheme, withTheme } from '@apache-superset/core/ui';
|
||||
import { isEqual } from 'lodash';
|
||||
|
||||
const seedRandom = seed('superset-ui');
|
||||
|
||||
export type PlainObject = Record<string, unknown>;
|
||||
|
||||
// Polyfill Word type since it's not exported from 'd3-cloud'
|
||||
export type Word = {
|
||||
text: string;
|
||||
@@ -49,15 +46,27 @@ export const ROTATION = {
|
||||
|
||||
export type RotationType = keyof typeof ROTATION;
|
||||
|
||||
export type WordCloudEncoding = DeriveEncoding<WordCloudEncodingConfig>;
|
||||
|
||||
type WordCloudEncodingConfig = {
|
||||
color: ['Color', string];
|
||||
fontFamily: ['Category', string];
|
||||
fontSize: ['Numeric', number];
|
||||
fontWeight: ['Category', string | number];
|
||||
text: ['Text', string];
|
||||
};
|
||||
/**
|
||||
* Encoding configuration for mapping data fields to visual properties.
|
||||
* Supports field-based mappings with optional scale configurations.
|
||||
*/
|
||||
export interface WordCloudEncoding {
|
||||
color?: {
|
||||
field?: string;
|
||||
value?: string;
|
||||
scale?: { scheme?: string };
|
||||
type?: string;
|
||||
};
|
||||
fontFamily?: { field?: string; value?: string };
|
||||
fontSize?: {
|
||||
field?: string;
|
||||
value?: number;
|
||||
scale?: { range?: [number, number]; zero?: boolean };
|
||||
type?: string;
|
||||
};
|
||||
fontWeight?: { field?: string; value?: string | number };
|
||||
text?: { field?: string; value?: string };
|
||||
}
|
||||
|
||||
export interface WordCloudVisualProps {
|
||||
encoding?: Partial<WordCloudEncoding>;
|
||||
@@ -88,38 +97,118 @@ type FullWordCloudProps = WordCloudProps &
|
||||
const SCALE_FACTOR_STEP = 0.5;
|
||||
const MAX_SCALE_FACTOR = 3;
|
||||
const TOP_RESULTS_PERCENTAGE = 0.1;
|
||||
|
||||
/**
|
||||
* Simple encoder that maps data fields to visual properties.
|
||||
* Replaces the encodable library with direct field access and d3 scales.
|
||||
*/
|
||||
class SimpleEncoder {
|
||||
private encoding: WordCloudEncoding;
|
||||
|
||||
private defaults: {
|
||||
color: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
fontWeight: string | number;
|
||||
text: string;
|
||||
};
|
||||
|
||||
private fontSizeScale: ((value: number) => number) | null = null;
|
||||
|
||||
constructor(
|
||||
encoding: WordCloudEncoding,
|
||||
defaults: {
|
||||
color: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
fontWeight: string | number;
|
||||
text: string;
|
||||
},
|
||||
) {
|
||||
this.encoding = encoding;
|
||||
this.defaults = defaults;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set domain from dataset to configure scales
|
||||
*/
|
||||
setDomainFromDataset(data: PlainObject[]): void {
|
||||
const fontSizeConfig = this.encoding.fontSize;
|
||||
if (fontSizeConfig?.field && fontSizeConfig?.scale?.range) {
|
||||
const values = data
|
||||
.map(d => Number(d[fontSizeConfig.field!]) || 0)
|
||||
.filter(v => !Number.isNaN(v));
|
||||
|
||||
if (values.length > 0) {
|
||||
const min = fontSizeConfig.scale.zero ? 0 : Math.min(...values);
|
||||
const max = Math.max(...values);
|
||||
const [rangeMin, rangeMax] = fontSizeConfig.scale.range;
|
||||
|
||||
this.fontSizeScale = scaleLinear()
|
||||
.domain([min, max])
|
||||
.range([rangeMin, rangeMax]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getText(d: PlainObject): string {
|
||||
const config = this.encoding.text;
|
||||
if (config?.field && d[config.field] !== undefined) {
|
||||
return String(d[config.field]);
|
||||
}
|
||||
return config?.value ?? this.defaults.text;
|
||||
}
|
||||
|
||||
getFontSize(d: PlainObject): number {
|
||||
const config = this.encoding.fontSize;
|
||||
if (config?.field && d[config.field] !== undefined) {
|
||||
const value = Number(d[config.field]) || 0;
|
||||
if (this.fontSizeScale) {
|
||||
return this.fontSizeScale(value);
|
||||
}
|
||||
return value || this.defaults.fontSize;
|
||||
}
|
||||
return config?.value ?? this.defaults.fontSize;
|
||||
}
|
||||
|
||||
getColor(d: PlainObject): string {
|
||||
const config = this.encoding.color;
|
||||
if (config?.field && d[config.field] !== undefined) {
|
||||
return String(d[config.field]);
|
||||
}
|
||||
return config?.value ?? this.defaults.color;
|
||||
}
|
||||
|
||||
getFontFamily(d: PlainObject): string {
|
||||
const config = this.encoding.fontFamily;
|
||||
if (config?.field && d[config.field] !== undefined) {
|
||||
return String(d[config.field]);
|
||||
}
|
||||
return config?.value ?? this.defaults.fontFamily;
|
||||
}
|
||||
|
||||
getFontWeight(d: PlainObject): string | number {
|
||||
const config = this.encoding.fontWeight;
|
||||
if (config?.field && d[config.field] !== undefined) {
|
||||
return d[config.field] as string | number;
|
||||
}
|
||||
return config?.value ?? this.defaults.fontWeight;
|
||||
}
|
||||
}
|
||||
|
||||
class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
static defaultProps = defaultProps;
|
||||
|
||||
isComponentMounted = false;
|
||||
|
||||
wordCloudEncoderFactory = createEncoderFactory<WordCloudEncodingConfig>({
|
||||
channelTypes: {
|
||||
color: 'Color',
|
||||
fontFamily: 'Category',
|
||||
fontSize: 'Numeric',
|
||||
fontWeight: 'Category',
|
||||
text: 'Text',
|
||||
},
|
||||
defaultEncoding: {
|
||||
color: { value: this.props.theme.colorTextLabel },
|
||||
fontFamily: { value: this.props.theme.fontFamily },
|
||||
fontSize: { value: 20 },
|
||||
fontWeight: { value: 'bold' },
|
||||
text: { value: '' },
|
||||
},
|
||||
});
|
||||
|
||||
createEncoder = (
|
||||
encoding?: Partial<WordCloudEncoding>,
|
||||
): Encoder<WordCloudEncodingConfig> => {
|
||||
const selector: (
|
||||
e: Partial<WordCloudEncoding>,
|
||||
) => Encoder<WordCloudEncodingConfig> =
|
||||
this.wordCloudEncoderFactory.createSelector();
|
||||
|
||||
return selector(encoding ?? {});
|
||||
};
|
||||
createEncoder = (encoding?: Partial<WordCloudEncoding>): SimpleEncoder =>
|
||||
new SimpleEncoder(encoding ?? {}, {
|
||||
color: this.props.theme.colorTextLabel,
|
||||
fontFamily: this.props.theme.fontFamily,
|
||||
fontSize: 20,
|
||||
fontWeight: 'bold',
|
||||
text: '',
|
||||
});
|
||||
|
||||
constructor(props: FullWordCloudProps) {
|
||||
super(props);
|
||||
@@ -165,9 +254,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
encoder.setDomainFromDataset(data);
|
||||
|
||||
const sortedData = [...data].sort(
|
||||
(a, b) =>
|
||||
encoder.channels.fontSize.encodeDatum(b, 0) -
|
||||
encoder.channels.fontSize.encodeDatum(a, 0),
|
||||
(a, b) => encoder.getFontSize(b) - encoder.getFontSize(a),
|
||||
);
|
||||
const topResultsCount = Math.max(
|
||||
sortedData.length * TOP_RESULTS_PERCENTAGE,
|
||||
@@ -177,15 +264,13 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
|
||||
this.generateCloud(encoder, 1, (words: Word[]) =>
|
||||
topResults.every((d: PlainObject) =>
|
||||
words.find(
|
||||
({ text }) => encoder.channels.text.getValueFromDatum(d) === text,
|
||||
),
|
||||
words.find(({ text }) => encoder.getText(d) === text),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
generateCloud(
|
||||
encoder: Encoder<WordCloudEncodingConfig>,
|
||||
encoder: SimpleEncoder,
|
||||
scaleFactor: number,
|
||||
isValid: (word: Word[]) => boolean,
|
||||
) {
|
||||
@@ -196,14 +281,10 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
.words(data.map((d: Word) => ({ ...d })))
|
||||
.padding(5)
|
||||
.rotate(ROTATION[rotation] || ROTATION.flat)
|
||||
.text((d: PlainObject) => encoder.channels.text.getValueFromDatum(d))
|
||||
.font((d: PlainObject) =>
|
||||
encoder.channels.fontFamily.encodeDatum(d, this.props.theme.fontFamily),
|
||||
)
|
||||
.fontWeight((d: PlainObject) =>
|
||||
encoder.channels.fontWeight.encodeDatum(d, 'normal'),
|
||||
)
|
||||
.fontSize((d: PlainObject) => encoder.channels.fontSize.encodeDatum(d, 0))
|
||||
.text((d: PlainObject) => encoder.getText(d))
|
||||
.font((d: PlainObject) => encoder.getFontFamily(d))
|
||||
.fontWeight((d: PlainObject) => encoder.getFontWeight(d))
|
||||
.fontSize((d: PlainObject) => encoder.getFontSize(d))
|
||||
.on('end', (words: Word[]) => {
|
||||
if (isValid(words) || scaleFactor > MAX_SCALE_FACTOR) {
|
||||
this.setWords(words);
|
||||
@@ -219,7 +300,6 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
const { width, height, encoding, sliceId, colorScheme } = this.props;
|
||||
|
||||
const encoder = this.createEncoder(encoding);
|
||||
encoder.channels.color.setDomainFromDataset(words);
|
||||
|
||||
const colorFn = CategoricalColorNamespace.getScale(colorScheme);
|
||||
const viewBoxWidth = width * scaleFactor;
|
||||
@@ -238,10 +318,7 @@ class WordCloud extends PureComponent<FullWordCloudProps, WordCloudState> {
|
||||
fontSize={`${w.size}px`}
|
||||
fontWeight={w.weight}
|
||||
fontFamily={w.font}
|
||||
fill={colorFn(
|
||||
encoder.channels.color.getValueFromDatum(w) as string,
|
||||
sliceId,
|
||||
)}
|
||||
fill={colorFn(encoder.getColor(w as PlainObject), sliceId)}
|
||||
textAnchor="middle"
|
||||
transform={`translate(${w.x}, ${w.y}) rotate(${w.rotate})`}
|
||||
>
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF 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 {
|
||||
Encodable,
|
||||
ColorSchemeResolver,
|
||||
TimeFormatResolver,
|
||||
CategoricalColorScaleResolver,
|
||||
defaultColorSchemeResolver,
|
||||
addPrefix,
|
||||
} from 'encodable';
|
||||
import {
|
||||
CategoricalColorNamespace,
|
||||
getCategoricalSchemeRegistry,
|
||||
getSequentialSchemeRegistry,
|
||||
getNumberFormatter,
|
||||
getTimeFormatter,
|
||||
LOCAL_PREFIX,
|
||||
getTimeFormatterRegistry,
|
||||
} from '@superset-ui/core';
|
||||
|
||||
const timeFormat: TimeFormatResolver = ({
|
||||
format,
|
||||
formatInLocalTime = false,
|
||||
} = {}) => {
|
||||
const formatString = formatInLocalTime
|
||||
? addPrefix(
|
||||
LOCAL_PREFIX,
|
||||
format ?? getTimeFormatterRegistry().getDefaultKey()!,
|
||||
)
|
||||
: format;
|
||||
|
||||
return getTimeFormatter(formatString);
|
||||
};
|
||||
|
||||
const colorSchemeResolver: ColorSchemeResolver = ({
|
||||
name,
|
||||
type = 'categorical',
|
||||
} = {}) => {
|
||||
if (type === 'sequential') {
|
||||
const scheme = getSequentialSchemeRegistry().get(name);
|
||||
|
||||
return typeof scheme === 'undefined'
|
||||
? scheme
|
||||
: { type: 'sequential', ...scheme };
|
||||
}
|
||||
if (type === 'categorical') {
|
||||
const scheme = getCategoricalSchemeRegistry().get(name);
|
||||
|
||||
return typeof scheme === 'undefined'
|
||||
? scheme
|
||||
: { type: 'categorical', ...scheme };
|
||||
}
|
||||
return defaultColorSchemeResolver({ name, type });
|
||||
};
|
||||
|
||||
const colorScaleResolver: CategoricalColorScaleResolver = ({
|
||||
name,
|
||||
namespace,
|
||||
} = {}) => CategoricalColorNamespace.getScale(name, namespace);
|
||||
|
||||
export default function configureEncodable() {
|
||||
Encodable.setNumberFormatResolver(getNumberFormatter)
|
||||
.setTimeFormatResolver(timeFormat)
|
||||
.setColorSchemeResolver(colorSchemeResolver)
|
||||
.setCategoricalColorScaleResolver(colorScaleResolver);
|
||||
}
|
||||
@@ -19,4 +19,3 @@
|
||||
|
||||
export { default as WordCloudChartPlugin } from './plugin';
|
||||
export * from './types';
|
||||
export { default as configureEncodable } from './configureEncodable';
|
||||
|
||||
@@ -29,9 +29,6 @@ import example1Dark from '../images/Word_Cloud-dark.jpg';
|
||||
import example2 from '../images/Word_Cloud_2.jpg';
|
||||
import example2Dark from '../images/Word_Cloud_2-dark.jpg';
|
||||
import controlPanel from './controlPanel';
|
||||
import configureEncodable from '../configureEncodable';
|
||||
|
||||
configureEncodable();
|
||||
|
||||
const metadata = new ChartMetadata({
|
||||
category: t('Ranking'),
|
||||
|
||||
Reference in New Issue
Block a user