From f11d3b710140979e675c86edebc65f68332af7d6 Mon Sep 17 00:00:00 2001 From: Ramil Minyukov <41204824+MrRamka@users.noreply.github.com> Date: Tue, 25 Jan 2022 17:08:04 +0300 Subject: [PATCH] Gradient scale (#886) * Add auto update property for gradient scales * Update label * Update styles * Added numerical color legend orientation * Removed 'Add legend' button for boolean scales * Clean code * Added legend height and width * Updated migrator * Added const value and update migrator Co-authored-by: Ramil Minyukov (Akvelon INC) Co-authored-by: bongshin --- package.json | 2 +- src/app/stores/migrator.ts | 25 ++++ src/app/views/panels/scale_editor.tsx | 4 +- .../controls/fluentui_input_number.tsx | 5 +- .../views/panels/widgets/fluentui_manager.tsx | 2 +- src/container/chart_template.ts | 12 -- src/core/prototypes/controls.ts | 2 + .../prototypes/legends/categorical_legend.ts | 13 +- src/core/prototypes/legends/color_legend.ts | 119 ++++++++++++++++-- src/core/prototypes/legends/types.ts | 7 ++ src/core/prototypes/plot_segments/axis.ts | 11 -- src/core/prototypes/scales/linear.ts | 37 +++++- src/strings.ts | 1 + 13 files changed, 190 insertions(+), 50 deletions(-) create mode 100644 src/core/prototypes/legends/types.ts diff --git a/package.json b/package.json index e259ffe3..c98b8bd6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "charticulator", - "version": "2.1.4", + "version": "2.1.5", "private": true, "author": { "name": "Donghao Ren", diff --git a/src/app/stores/migrator.ts b/src/app/stores/migrator.ts index fabdd9e6..68979e1e 100644 --- a/src/app/stores/migrator.ts +++ b/src/app/stores/migrator.ts @@ -34,6 +34,8 @@ import { TickFormatType } from "../../core/specification/types"; import { SymbolElementProperties } from "../../core/prototypes/marks/symbol.attrs"; import { LinearBooleanScaleMode } from "../../core/prototypes/scales/linear"; import { parseDerivedColumnsExpression } from "../../core/prototypes/plot_segments/utils"; +import { OrientationType } from "../../core/prototypes/legends/types"; +import { NumericalColorLegendClass } from "../../core/prototypes/legends/color_legend"; /** Upgrade old versions of chart spec and state to newer version */ export class Migrator { @@ -176,6 +178,13 @@ export class Migrator { state = this.setMissedSortProperties(state); } + if ( + compareVersion(state.version, "2.1.5") < 0 && + compareVersion(targetVersion, "2.1.5") >= 0 + ) { + state = this.setMissedLegendProperties(state); + } + // After migration, set version to targetVersion state.version = targetVersion; @@ -924,4 +933,20 @@ export class Migrator { } return state; } + + public setMissedLegendProperties(state: AppStoreState) { + for (const element of state.chart.elements) { + if (Prototypes.isType(element.classID, "legend.numerical-color")) { + const legend = element as ChartElement; + if (legend.properties.orientation === undefined) { + legend.properties.orientation = OrientationType.VERTICAL; + } + if (legend.properties.length === undefined) { + legend.properties.length = + NumericalColorLegendClass.defaultLegendLength; + } + } + } + return state; + } } diff --git a/src/app/views/panels/scale_editor.tsx b/src/app/views/panels/scale_editor.tsx index 10dd4208..9f2b961d 100644 --- a/src/app/views/panels/scale_editor.tsx +++ b/src/app/views/panels/scale_editor.tsx @@ -56,7 +56,9 @@ export class ScaleEditor extends React.Component< let canAddLegend = true; if ( scale.classID.startsWith("scale.format") || - scale.classID === "scale.categorical" + scale.classID === "scale.categorical" || + scale.classID === "scale.categorical" || + scale.classID === "scale.linear" ) { canAddLegend = false; } diff --git a/src/app/views/panels/widgets/controls/fluentui_input_number.tsx b/src/app/views/panels/widgets/controls/fluentui_input_number.tsx index 0cae1b7b..d5b85e89 100644 --- a/src/app/views/panels/widgets/controls/fluentui_input_number.tsx +++ b/src/app/views/panels/widgets/controls/fluentui_input_number.tsx @@ -20,6 +20,7 @@ import { labelRender, PlaceholderStyle, } from "./fluentui_customized_components"; +import { CSSProperties } from "react"; export interface InputNumberProps { defaultValue?: number; @@ -43,6 +44,8 @@ export interface InputNumberProps { label?: string; stopPropagation?: boolean; + + styles?: CSSProperties; } export const FluentInputNumber: React.FC = (props) => { @@ -197,7 +200,7 @@ export const FluentInputNumber: React.FC = (props) => { {props.showSlider ? ( ) : null} - + {props.showUpdown ? ( renderUpdown() diff --git a/src/app/views/panels/widgets/fluentui_manager.tsx b/src/app/views/panels/widgets/fluentui_manager.tsx index 43fe261b..7d6cce2b 100644 --- a/src/app/views/panels/widgets/fluentui_manager.tsx +++ b/src/app/views/panels/widgets/fluentui_manager.tsx @@ -604,7 +604,7 @@ export class FluentUIWidgetManager {options.headerLabel ? ( ) : null} - + a.concat(b), []); const scaleClass = Prototypes.ObjectClasses.Create(null, object, { attributes: {}, diff --git a/src/core/prototypes/controls.ts b/src/core/prototypes/controls.ts index 1737ee68..cf783a5c 100644 --- a/src/core/prototypes/controls.ts +++ b/src/core/prototypes/controls.ts @@ -70,6 +70,7 @@ export interface InputBooleanOptions { observerConfig?: ObserverConfig; checkBoxStyles?: ICheckboxStyles; onChange?: (value: boolean) => void; + styles?: CSSProperties; } export interface RowOptions { @@ -144,6 +145,7 @@ export interface InputNumberOptions { stopPropagation?: boolean; observerConfig?: ObserverConfig; + styles?: CSSProperties; } export interface InputDateOptions { diff --git a/src/core/prototypes/legends/categorical_legend.ts b/src/core/prototypes/legends/categorical_legend.ts index c7ea4dbd..63b69fdc 100644 --- a/src/core/prototypes/legends/categorical_legend.ts +++ b/src/core/prototypes/legends/categorical_legend.ts @@ -8,6 +8,7 @@ import { LegendClass, LegendProperties } from "./legend"; import { Controls } from ".."; import { strings } from "../../../strings"; import { CharticulatorPropertyAccessors } from "../../../app/views/panels/widgets/types"; +import { OrientationType } from "./types"; export interface CategoricalLegendItem { type: "number" | "color" | "boolean"; @@ -23,7 +24,7 @@ export class CategoricalLegendClass extends LegendClass { public static defaultProperties: LegendProperties = { ...LegendClass.defaultProperties, - orientation: "vertical", + orientation: OrientationType.VERTICAL, }; protected textMeasure = new Graphics.TextMeasurer(); @@ -93,7 +94,7 @@ export class CategoricalLegendClass extends LegendClass { public getLineWidth(): number { let width = 0; const items = this.getLegendItems(); - if (this.object.properties.orientation === "horizontal") { + if (this.object.properties.orientation === OrientationType.HORIZONTAL) { for (let i = 0; i < items.length; i++) { const item = items[i]; const metrics = this.textMeasure.measure(item.label); @@ -116,7 +117,7 @@ export class CategoricalLegendClass extends LegendClass { public getLegendSize(): [number, number] { const items = this.getLegendItems(); if ( - this.object.properties.orientation === "vertical" || + this.object.properties.orientation === OrientationType.VERTICAL || this.object.properties.orientation === undefined ) { return [ @@ -215,7 +216,7 @@ export class CategoricalLegendClass extends LegendClass { } break; } - if (this.object.properties.orientation === "horizontal") { + if (this.object.properties.orientation === OrientationType.HORIZONTAL) { gItem.transform = { x: itemGroupOffset, y: 0, @@ -238,7 +239,7 @@ export class CategoricalLegendClass extends LegendClass { public getLayoutBox(): { x1: number; y1: number; x2: number; y2: number } { if ( - this.object.properties.orientation === "vertical" || + this.object.properties.orientation === OrientationType.VERTICAL || this.object.properties.orientation === undefined ) { return super.getLayoutBox(); @@ -300,7 +301,7 @@ export class CategoricalLegendClass extends LegendClass { strings.objects.legend.vertical, strings.objects.legend.horizontal, ], - options: ["vertical", "horizontal"], + options: [OrientationType.VERTICAL, OrientationType.HORIZONTAL], label: strings.objects.legend.orientation, } ), diff --git a/src/core/prototypes/legends/color_legend.ts b/src/core/prototypes/legends/color_legend.ts index 6aea3001..c6e8a783 100644 --- a/src/core/prototypes/legends/color_legend.ts +++ b/src/core/prototypes/legends/color_legend.ts @@ -6,20 +6,53 @@ import * as Graphics from "../../graphics"; import * as Specification from "../../specification"; import { AxisRenderer } from "../plot_segments/axis"; -import { LegendClass } from "./legend"; +import { LegendClass, LegendProperties } from "./legend"; +import { Controls } from "../common"; +import { CharticulatorPropertyAccessors } from "../../../app/views/panels/widgets/types"; +import { strings } from "../../../strings"; +import { OrientationType } from "./types"; export class NumericalColorLegendClass extends LegendClass { public static classID: string = "legend.numerical-color"; public static type: string = "legend"; + public static defaultLegendLength: number = 100; + + public static defaultProperties: LegendProperties = { + ...LegendClass.defaultProperties, + orientation: OrientationType.VERTICAL, + length: NumericalColorLegendClass.defaultLegendLength, + }; + private gradientWidth: number = 12; + + public getLineHeight(): number { + return this.object.properties.fontSize + 25 + this.gradientWidth; + } + public getLegendSize(): [number, number] { - return [100, 100]; + const props = this.object.properties; + const length = props.length + ? +props.length + : NumericalColorLegendClass.defaultLegendLength; + if (this.isHorizontalOrientation()) { + return [length, this.getLineHeight()]; + } + return [this.getLineHeight(), length]; + } + + private isHorizontalOrientation(): boolean { + const props = this.object.properties; + return props.orientation === OrientationType.HORIZONTAL; } public getGraphics(): Graphics.Element { - const height = this.getLegendSize()[1]; + const height = this.isHorizontalOrientation() + ? this.getLegendSize()[0] + : this.getLegendSize()[1]; const marginLeft = 5; const gradientWidth = 12; + const axisMargin = 2; + const horizontalShift = this.getLegendSize()[1] - gradientWidth; const scale = this.getScale(); if (!scale) { @@ -39,9 +72,18 @@ export class NumericalColorLegendClass extends LegendClass { lineColor: this.object.properties.textColor, }); const g = Graphics.makeGroup([]); - g.elements.push( - axisRenderer.renderLine(marginLeft + gradientWidth + 2, 0, 90, 1) - ); + if (this.isHorizontalOrientation()) { + g.elements.push(axisRenderer.renderLine(0, -axisMargin, 0, 1)); + } else { + g.elements.push( + axisRenderer.renderLine( + marginLeft + gradientWidth + axisMargin, + 0, + 90, + 1 + ) + ); + } const ticks = height * 2; const interp = interpolateColors(range.colors, range.colorspace); @@ -50,15 +92,68 @@ export class NumericalColorLegendClass extends LegendClass { const color = interp(t); const y1 = (i / ticks) * height; const y2 = Math.min(height, ((i + 1.5) / ticks) * height); - g.elements.push( - Graphics.makeRect(marginLeft, y1, marginLeft + gradientWidth, y2, { - fillColor: color, - }) - ); + if (this.isHorizontalOrientation()) { + g.elements.push( + Graphics.makeRect(y1, 0, y2, gradientWidth, { + fillColor: color, + }) + ); + } else { + g.elements.push( + Graphics.makeRect(marginLeft, y1, marginLeft + gradientWidth, y2, { + fillColor: color, + }) + ); + } } const { x1, y1 } = this.getLayoutBox(); - g.transform = { x: x1, y: y1, angle: 0 }; + if (this.isHorizontalOrientation()) { + g.transform = { x: x1, y: y1 + horizontalShift, angle: 0 }; + } else { + g.transform = { x: x1, y: y1, angle: 0 }; + } return g; } + + public getAttributePanelWidgets( + manager: Controls.WidgetManager & CharticulatorPropertyAccessors + ): Controls.Widget[] { + const widgets = super.getAttributePanelWidgets(manager); + + return [ + ...widgets, + manager.verticalGroup( + { + header: strings.objects.legend.numericalColorLegend, + }, + [ + manager.inputNumber( + { property: "length" }, + { + label: this.isHorizontalOrientation() + ? strings.objects.width + : strings.objects.height, + updownTick: 10, + showUpdown: true, + } + ), + manager.inputSelect( + { property: "orientation" }, + { + type: "radio", + showLabel: false, + icons: ["AlignHorizontalCenter", "AlignVerticalCenter"], + labels: [ + strings.objects.legend.vertical, + strings.objects.legend.horizontal, + ], + options: [OrientationType.VERTICAL, OrientationType.HORIZONTAL], + label: strings.objects.legend.orientation, + } + ), + ] + ), + ]; + } } diff --git a/src/core/prototypes/legends/types.ts b/src/core/prototypes/legends/types.ts new file mode 100644 index 00000000..7f9e4301 --- /dev/null +++ b/src/core/prototypes/legends/types.ts @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT license. + +export enum OrientationType { + VERTICAL = "vertical", + HORIZONTAL = "horizontal", +} diff --git a/src/core/prototypes/plot_segments/axis.ts b/src/core/prototypes/plot_segments/axis.ts index 803325a4..578dba41 100644 --- a/src/core/prototypes/plot_segments/axis.ts +++ b/src/core/prototypes/plot_segments/axis.ts @@ -1469,17 +1469,6 @@ export function buildAxisAppearanceWidgets( allowNull: true, } ), - manager.inputFormat( - { - property: axisProperty, - field: "tickFormat", - }, - { - blank: strings.core.auto, - isDateField: false, - label: strings.objects.axes.tickFormat, - } - ), manager.inputNumber( { property: axisProperty, diff --git a/src/core/prototypes/scales/linear.ts b/src/core/prototypes/scales/linear.ts index d0fdc3f5..30b53025 100644 --- a/src/core/prototypes/scales/linear.ts +++ b/src/core/prototypes/scales/linear.ts @@ -19,8 +19,8 @@ import { InferParametersOptions } from "./scale"; export interface LinearScaleProperties extends Specification.AttributeMap { domainMin: number; domainMax: number; - autoDomainMin: number; - autoDomainMax: number; + autoDomainMin: boolean; + autoDomainMax: boolean; } export interface LinearScaleAttributes extends Specification.AttributeMap { @@ -154,7 +154,11 @@ export class LinearScale extends ScaleClass< ), manager.inputNumber( { property: "domainMax" }, - { label: strings.objects.dataAxis.end, stopPropagation: true } + { + label: strings.objects.dataAxis.end, + stopPropagation: true, + styles: { marginBottom: "0.5rem" }, + } ), manager.sectionHeader(strings.objects.dataAxis.autoUpdateValues), manager.inputBoolean( @@ -296,7 +300,6 @@ export class LinearColorScale extends ScaleClass< const s = new Scale.LinearScale(); const values = column.filter((x) => typeof x == "number"); s.inferParameters(values); - s.adjustDomain(options); if (options.extendScaleMin || props.domainMin === undefined) { props.domainMin = s.domainMin; @@ -321,7 +324,31 @@ export class LinearColorScale extends ScaleClass< ), manager.inputNumber( { property: "domainMax" }, - { stopPropagation: true, label: strings.objects.dataAxis.end } + { + stopPropagation: true, + label: strings.objects.dataAxis.end, + styles: { marginBottom: "0.5rem" }, + } + ), + manager.sectionHeader(strings.objects.dataAxis.autoUpdateValues), + manager.inputBoolean( + { + property: "autoDomainMin", + }, + { + type: "checkbox", + label: strings.objects.dataAxis.autoMin, + } + ), + manager.inputBoolean( + { + property: "autoDomainMax", + }, + { + type: "checkbox", + label: strings.objects.dataAxis.autoMax, + styles: { marginBottom: "0.5rem" }, + } ), manager.sectionHeader(strings.objects.dataAxis.gradient), manager.inputColorGradient( diff --git a/src/strings.ts b/src/strings.ts index 54dd6e75..97394fa2 100644 --- a/src/strings.ts +++ b/src/strings.ts @@ -492,6 +492,7 @@ export const strings = { labels: "Labels", layout: "Layout", categoricalLegend: "Categorical legend", + numericalColorLegend: "Numerical color legend", ordering: "Ordering", }, links: {