ggaction 0.0.16 → 0.0.18
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.
- package/CHANGELOG.md +66 -0
- package/README.md +3 -2
- package/knowledge/action-card.schema.json +1 -1
- package/knowledge/action-cards.json +1 -1
- package/knowledge/intent-taxonomy.json +1 -1
- package/knowledge/mcp-resources.json +1 -1
- package/knowledge/task-resolver.js +1 -1
- package/package.json +10 -4
- package/src/BasicChartProgram.js +1 -1
- package/src/accessibility.js +3 -2
- package/src/actions/boxPlots/create.js +14 -4
- package/src/actions/boxPlots/edit.js +39 -33
- package/src/actions/boxPlots/materialize.js +7 -3
- package/src/actions/boxPlots/options.js +37 -11
- package/src/actions/canvas/actions.js +70 -95
- package/src/actions/canvas/fitting.js +16 -1
- package/src/actions/canvas/index.js +3 -0
- package/src/actions/canvas/plotBorder.js +28 -0
- package/src/actions/charts/area.js +2 -2
- package/src/actions/charts/bar.js +34 -7
- package/src/actions/charts/ecdf.js +4 -18
- package/src/actions/charts/endpoints.js +4 -18
- package/src/actions/charts/heatmap.js +4 -4
- package/src/actions/charts/index.js +2 -0
- package/src/actions/charts/interval-regression.js +2 -2
- package/src/actions/charts/line.js +1 -1
- package/src/actions/charts/pie.js +3 -47
- package/src/actions/charts/radial.js +34 -12
- package/src/actions/charts/scatter.js +16 -2
- package/src/actions/charts/shared.js +2 -2
- package/src/actions/charts/text.js +35 -0
- package/src/actions/composition/actions.js +6 -2
- package/src/actions/data/bin.js +1 -1
- package/src/actions/data/box.js +2 -1
- package/src/actions/data/create.js +18 -5
- package/src/actions/data/density.js +1 -1
- package/src/actions/data/derived.js +15 -1
- package/src/actions/data/edit.js +20 -7
- package/src/actions/data/filter.js +15 -4
- package/src/actions/data/index.js +3 -0
- package/src/actions/data/interval.js +2 -0
- package/src/actions/data/regression.js +6 -2
- package/src/actions/data/revise.js +37 -5
- package/src/actions/data/shared.js +33 -4
- package/src/actions/data/sort.js +21 -0
- package/src/actions/data/stack.js +3 -1
- package/src/actions/data/summary.js +1 -1
- package/src/actions/encodings/angle.js +4 -1
- package/src/actions/encodings/appearance.js +43 -31
- package/src/actions/encodings/barWidth.js +36 -11
- package/src/actions/encodings/channels.js +0 -1
- package/src/actions/encodings/color/continuous.js +5 -2
- package/src/actions/encodings/color/index.js +5 -2
- package/src/actions/encodings/color/policy.js +10 -3
- package/src/actions/encodings/density/resolve.js +1 -6
- package/src/actions/encodings/offset.js +4 -1
- package/src/actions/encodings/position/policies/arc.js +12 -10
- package/src/actions/encodings/position/policies/area.js +3 -3
- package/src/actions/encodings/position/policies/bar.js +15 -6
- package/src/actions/encodings/position/policies/line.js +17 -6
- package/src/actions/encodings/position/resolve.js +9 -3
- package/src/actions/encodings/ranged.js +23 -1
- package/src/actions/encodings/remove.js +3 -2
- package/src/actions/encodings/ruleAppearance.js +4 -1
- package/src/actions/encodings/seriesLayout.js +1 -0
- package/src/actions/encodings/stroke.js +4 -1
- package/src/actions/encodings/strokeDash.js +6 -1
- package/src/actions/encodings/text.js +12 -4
- package/src/actions/errorBands/create.js +2 -2
- package/src/actions/errorBands/edit.js +2 -1
- package/src/actions/facets/actions.js +158 -240
- package/src/actions/gradientPlots/resolve.js +2 -17
- package/src/actions/guides/applicability.js +1 -1
- package/src/actions/guides/axes/axis.js +16 -4
- package/src/actions/guides/axes/edit.js +4 -4
- package/src/actions/guides/axes/labels.js +120 -46
- package/src/actions/guides/axes/lines.js +6 -5
- package/src/actions/guides/axes/parallel/lifecycle.js +1 -1
- package/src/actions/guides/axes/parallel/policy.js +8 -7
- package/src/actions/guides/axes/remove.js +3 -2
- package/src/actions/guides/axes/tickGroups.js +5 -2
- package/src/actions/guides/axes/ticks.js +6 -3
- package/src/actions/guides/axes/titles.js +71 -26
- package/src/actions/guides/basic.js +2 -0
- package/src/actions/guides/facadeLegend.js +10 -2
- package/src/actions/guides/grids/grid.js +7 -1
- package/src/actions/guides/grids/resolve.js +59 -12
- package/src/actions/guides/legends/blocks.js +3 -1
- package/src/actions/guides/legends/categorical/actions.js +70 -17
- package/src/actions/guides/legends/categorical/components.js +12 -8
- package/src/actions/guides/legends/categorical/index.js +6 -0
- package/src/actions/guides/legends/categorical/layout.js +37 -8
- package/src/actions/guides/legends/categorical/options.js +31 -20
- package/src/actions/guides/legends/categorical/resolve.js +2 -2
- package/src/actions/guides/legends/categorical/symbols.js +18 -12
- package/src/actions/guides/legends/categorical/validation.js +1 -6
- package/src/actions/guides/legends/continuous/colorSymbols.js +1 -0
- package/src/actions/guides/legends/continuous/common.js +1 -447
- package/src/actions/guides/legends/continuous/gradient.js +47 -34
- package/src/actions/guides/legends/continuous/interval.js +4 -4
- package/src/actions/guides/legends/continuous/opacity.js +19 -12
- package/src/actions/guides/legends/edit.js +1 -604
- package/src/actions/guides/legends/focused.js +2 -2
- package/src/actions/guides/legends/group.js +1 -0
- package/src/actions/guides/legends/lane.js +1 -516
- package/src/actions/guides/legends/remove.js +3 -0
- package/src/actions/guides/legends/sampling.js +1 -1
- package/src/actions/guides/legends/size.js +51 -43
- package/src/actions/guides/legends/strokeWidth.js +11 -29
- package/src/actions/guides/legends/target.js +3 -3
- package/src/actions/guides/legends/transition.js +21 -7
- package/src/actions/guides/polar/axes/labels.js +4 -2
- package/src/actions/guides/polar/axes/titles.js +7 -5
- package/src/actions/guides/polar/resolve.js +6 -14
- package/src/actions/index.js +16 -16
- package/src/actions/marks/arc/actions.js +3 -10
- package/src/actions/marks/area/actions.js +8 -5
- package/src/actions/marks/area/materialize.js +8 -9
- package/src/actions/marks/bar/create.js +17 -4
- package/src/actions/marks/bar/edit.js +18 -7
- package/src/actions/marks/bar/materialize.js +9 -6
- package/src/actions/marks/line/actions.js +11 -4
- package/src/actions/marks/line/materialize.js +9 -5
- package/src/actions/marks/point/create.js +8 -1
- package/src/actions/marks/point/edit.js +13 -6
- package/src/actions/marks/point/materialize.js +4 -2
- package/src/actions/marks/rect/actions.js +26 -11
- package/src/actions/marks/remove.js +2 -0
- package/src/actions/marks/rule/actions.js +31 -12
- package/src/actions/marks/text/actions.js +42 -11
- package/src/actions/marks/text/layout.js +3 -0
- package/src/actions/marks/tick/actions.js +23 -8
- package/src/actions/primitives/semanticAction.js +37 -3
- package/src/actions/primitives/semanticValidation/dataset.js +13 -1
- package/src/actions/primitives/semanticValidation/layer.js +26 -2
- package/src/actions/primitives/semanticValidation/scale.js +6 -0
- package/src/actions/regression/create.js +26 -5
- package/src/actions/regression/edit.js +50 -15
- package/src/actions/regression/follow.js +22 -0
- package/src/actions/regression/resolve.js +2 -6
- package/src/actions/scales/channels.js +20 -15
- package/src/actions/scales/consumers/common.js +2 -1
- package/src/actions/scales/consumers/families.js +16 -5
- package/src/actions/scales/create.js +10 -4
- package/src/actions/scales/definitions.js +1 -425
- package/src/actions/scales/edit.js +3 -3
- package/src/actions/scales/editPolicy.js +9 -3
- package/src/actions/scales/materialize.js +1 -1
- package/src/actions/scales/parallel.js +1 -1
- package/src/actions/scales/patch.js +1 -1
- package/src/actions/scales/preview.js +49 -2
- package/src/actions/scales/quantitativeColor.js +3 -3
- package/src/actions/selection/actions.js +1 -13
- package/src/actions/theme/reconcile.js +83 -27
- package/src/actions/theme/state.js +12 -0
- package/src/actions/titles/actions.js +5 -18
- package/src/actions/titles/resolve.js +41 -19
- package/src/actions/violinPlots/edit.js +3 -20
- package/src/core/action.js +3 -2
- package/src/core/compositionState.js +4 -1
- package/src/core/font.js +9 -2
- package/src/core/optionRequirements.js +1 -0
- package/src/core/programState.js +5 -1
- package/src/core/resourceReferences.js +3 -0
- package/src/core/textMetricProfile.js +3 -2
- package/src/core/textMetrics.js +41 -4
- package/src/core/validation.js +26 -0
- package/src/core/vocabulary.js +4 -0
- package/src/grammar/aggregate.js +4 -18
- package/src/grammar/arcs.js +1 -386
- package/src/grammar/areaPaint.js +10 -0
- package/src/grammar/areaSeries.js +18 -10
- package/src/grammar/bars/geometry.js +30 -3
- package/src/grammar/bars/policy.js +12 -1
- package/src/grammar/bin.js +41 -22
- package/src/grammar/bin2d.js +1 -6
- package/src/grammar/boxPlot.js +56 -9
- package/src/grammar/categoryOrder.js +1 -6
- package/src/grammar/complete.js +15 -19
- package/src/grammar/computed.js +92 -25
- package/src/grammar/curveCommands.js +29 -16
- package/src/grammar/datasetSchema.js +382 -0
- package/src/grammar/density.js +53 -5
- package/src/grammar/displayLabels.js +28 -0
- package/src/grammar/ecdf.js +2 -9
- package/src/grammar/facets/dependencies.js +6 -2
- package/src/grammar/facets/guides.js +5 -0
- package/src/grammar/facets/index.js +2 -5
- package/src/grammar/filter.js +133 -39
- package/src/grammar/fold.js +2 -12
- package/src/grammar/gradientProfile.js +1 -6
- package/src/grammar/impute.js +21 -48
- package/src/grammar/interval.js +34 -10
- package/src/grammar/itemMissing.js +59 -0
- package/src/grammar/legendGradient.js +18 -0
- package/src/grammar/legendGroups.js +1 -0
- package/src/grammar/legendTitleStyle.js +22 -0
- package/src/grammar/lineSeries.js +32 -17
- package/src/grammar/markLabels.js +5 -2
- package/src/grammar/normalize.js +4 -54
- package/src/grammar/numeric.js +1 -290
- package/src/grammar/parallelCoordinates.js +1 -6
- package/src/grammar/parseTime.js +41 -0
- package/src/grammar/pathCommands.js +11 -4
- package/src/grammar/polar.js +1 -184
- package/src/grammar/positionCompatibility.js +3 -3
- package/src/grammar/regression/derive.js +63 -16
- package/src/grammar/regression/index.js +1 -0
- package/src/grammar/regression/models.js +116 -59
- package/src/grammar/regression/parameters.js +69 -6
- package/src/grammar/roundedRect.js +42 -54
- package/src/grammar/scales/appearance.js +1 -206
- package/src/grammar/scales/color.js +1 -382
- package/src/grammar/scales/colorConsumers.js +5 -2
- package/src/grammar/scales/definition.js +1 -190
- package/src/grammar/scales/discretized.js +1 -195
- package/src/grammar/scales/size.js +1 -456
- package/src/grammar/scales/temporal.js +2 -13
- package/src/grammar/scales/transformed.js +1 -383
- package/src/grammar/scales/types.js +1 -1
- package/src/grammar/scales/validation.js +1 -66
- package/src/grammar/schemas/concreteGraphic.js +11 -0
- package/src/grammar/schemas/graphic.js +3 -0
- package/src/grammar/schemas/semanticPath.js +5 -3
- package/src/grammar/sort.js +88 -0
- package/src/grammar/stack.js +2 -10
- package/src/grammar/statisticalReference.js +1 -6
- package/src/grammar/summary.js +120 -45
- package/src/grammar/text.js +19 -0
- package/src/grammar/timeUnit.js +16 -7
- package/src/grammar/transformKeys.js +47 -0
- package/src/grammar/transformTopology.js +1 -0
- package/src/grammar/transforms.js +25 -12
- package/src/grammar/valueFormat.js +17 -5
- package/src/grammar/weightedStatistics.js +1 -6
- package/src/grammar/window.js +4 -58
- package/src/inspection.js +495 -0
- package/src/inspectionDescriptors.js +3 -0
- package/src/layout/canvas.js +1 -110
- package/src/layout/facets.js +1 -244
- package/src/layout/labels.js +1 -0
- package/src/layout/legend.js +13 -0
- package/src/layout/legendGroup.js +1 -0
- package/src/layout/legendItems.js +59 -15
- package/src/layout/legendLane.js +1 -441
- package/src/layout/legendText.js +1 -0
- package/src/layout/title.js +35 -13
- package/src/materialization/bars/aggregate.js +11 -5
- package/src/materialization/bars/histogram.js +4 -1
- package/src/materialization/bars/ranged.js +19 -6
- package/src/materialization/bars/resolve.js +26 -3
- package/src/materialization/coordinateBounds.js +8 -0
- package/src/materialization/dependencies.js +4 -44
- package/src/materialization/facetGuides/layout.js +1 -0
- package/src/materialization/facetGuides/placement.js +19 -8
- package/src/materialization/facetGuides/preparation.js +15 -7
- package/src/materialization/facetHeaders.js +1 -347
- package/src/materialization/facets.js +1 -185
- package/src/materialization/graphicHierarchy.js +1 -1
- package/src/materialization/guides/layout.js +1 -57
- package/src/materialization/guides/resources.js +1 -123
- package/src/materialization/layout.js +53 -0
- package/src/materialization/legendGroups.js +1 -0
- package/src/materialization/legendText.js +1 -0
- package/src/materialization/legends.js +1 -1
- package/src/materialization/marks/capabilities.js +5 -6
- package/src/materialization/rect.js +1 -1
- package/src/materialization/revisionIdentity.js +19 -0
- package/src/materialization/scaleGuideDependencies.js +1 -1
- package/src/materialization/scales/map.js +3 -0
- package/src/materialization/scales/policies/arc.js +8 -7
- package/src/materialization/scales/policies/bar.js +19 -7
- package/src/materialization/scales/policies/series.js +2 -1
- package/src/materialization/scales/resolve.js +72 -7
- package/src/materialization/selection/items/arc.js +1 -1
- package/src/materialization/selection/items/bar.js +7 -6
- package/src/materialization/selection/items/path.js +7 -6
- package/src/materialization/selection/items/rect.js +2 -2
- package/src/materialization/text.js +17 -6
- package/src/materialization/textLines.js +15 -0
- package/src/persistence/codec.js +37 -6
- package/src/persistence/validation.js +47 -0
- package/src/persistence.js +94 -8
- package/src/renderers/canvas/circle.js +6 -3
- package/src/renderers/canvas/fill.js +7 -1
- package/src/renderers/canvas/index.js +2 -1
- package/src/renderers/canvas/line.js +1 -0
- package/src/renderers/canvas/path.js +1 -1
- package/src/renderers/canvas/rect.js +11 -8
- package/src/renderers/canvas/stroke.js +2 -1
- package/src/renderers/canvas/text.js +5 -3
- package/src/renderers/svg.js +4 -1
- package/src/selectors/layers.js +19 -0
- package/src/theme/defaults.js +8 -2
- package/src/version.js +1 -1
- package/types/basic.d.ts +1 -0
- package/types/index.d.ts +7 -0
- package/types/inspection.d.ts +17 -0
- package/types/program.d.ts +494 -112
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{legendResourcePolicy as O}from"../../../materialization/guides/resources.js";import{LEGEND_POSITIONS as E,LEGEND_BLOCK_LAYOUTS as x}from"../../../core/vocabulary.js";import{validateLegendGroupState as v}from"../../../grammar/legendGroups.js";import{validateOptionObject as G,validateNonNegativeFinite as C}from"../../../core/validation.js";import{PLOT_LEGEND_POSITIONS as m}from"../../../layout/legend.js";import{legendGroup as _}from"../../../materialization/legendGroups.js";const N=E;function j(n){if(Object.hasOwn(n,"at")){if(G(n.at,["x","y"],"Legend at"),!Number.isFinite(n.at.x)||!Number.isFinite(n.at.y))throw new TypeError("Legend at requires finite x and y coordinates.");if(["position","align","offset"].some(e=>Object.hasOwn(n,e)))throw new Error("Legend at conflicts with position, align and offset in the same call.")}if(Object.hasOwn(n,"blockLayout")&&!x.includes(n.blockLayout))throw new Error("Legend blockLayout must be row, column or wrap.")}function y(n,e){return n.at!==void 0||e.at!==void 0?n.at!==void 0&&e.at!==void 0&&n.at.x===e.at.x&&n.at.y===e.at.y:n.position===e.position&&n.align===e.align&&n.offset===e.offset}function P(n,e,r,{creation:u=!1,corner:f=!1,nativeOffset:o=30}={}){j(e);const t=_(n,r),s=t!==void 0||e.at!==void 0||e.blockLayout!==void 0||f,{at:p,blockLayout:a,...l}=e;if(!s)return{program:n,options:l,grouped:!1};const g=Object.entries(n.guideConfigs.legend??{}).filter(([,d])=>d.target===r),w=g.map(([,d])=>d),h=g.find(([d])=>["series","color","stroke"].includes(d))?.[1].offset??o;if(w.some(d=>d.layout==="legacy-bottom")||e.layout==="legacy-bottom")throw new Error("Legend groups require edge layout.");let i;if(e.at!==void 0)i={at:{...e.at}};else if(e.position!==void 0){if(!N.includes(e.position))throw new Error(`Unsupported legend position "${e.position}".`);const d=m.includes(e.position),c=t?.placement.position===e.position;i={position:e.position,align:e.align??(c?t.placement.align:"center"),offset:e.offset??(c?t.placement.offset:d?8:h)}}else if(t!==void 0){if(i=t.placement.at===void 0?{...t.placement,...e.align===void 0?{}:{align:e.align},...e.offset===void 0?{}:{offset:e.offset}}:t.placement,t.placement.at!==void 0&&(e.align!==void 0||e.offset!==void 0))throw new Error("Free legend placement requires position before align or offset.")}else{const d=w.map(c=>({position:c.position,align:c.align,offset:c.offset}));if(d.length>1&&!d.every(c=>y(c,d[0])))throw new Error("Legend blockLayout requires an explicit anchor when existing blocks disagree.");i=d[0]??{position:"right",align:"center",offset:h}}if(i.position!==void 0){if(!["left","center","right"].includes(i.align))throw new Error("Unsupported legend group alignment.");if((m.includes(i.position)||["left","right"].includes(i.position))&&i.align!=="center")throw new Error("Side and in-plot legend groups require center alignment.");C(i.offset,"Legend group offset")}if(u&&t!==void 0&&(e.at!==void 0||e.position!==void 0||e.align!==void 0||e.offset!==void 0)&&!y(i,t.placement))throw new Error("New legend block anchor conflicts with its target group.");if(u&&t!==void 0&&e.blockLayout!==void 0&&e.blockLayout!==t.blockLayout)throw new Error("New legend blockLayout conflicts with its target group.");const b=e.blockLayout??t?.blockLayout??(i.at!==void 0?"column":m.includes(i.position)?"row":"wrap"),L={placement:i,blockLayout:b};v({[r]:L});const k=n._withMaterializationConfig(["guides","legendGroups",r],L);return i.position!==void 0&&(l.position=i.position,l.offset=i.offset,l.align=i.align),{program:k,options:l,grouped:!0}}function K(n,e){if(e.target!==void 0)return e.target;const r=e.channels,u=n.semanticSpec.layers.filter(f=>r===void 0?Object.keys(f.encoding??{}).some(o=>["color","stroke","shape","strokeDash","size","opacity","strokeWidth"].includes(o)&&f.encoding[o]?.scale!==void 0):r.every(o=>f.encoding?.[o]?.scale!==void 0));if(u.length!==1)throw new Error("Legend group requires one eligible target or an explicit target.");return u[0].id}function M(n,e,r,{creation:u=!1}={}){let f=e;for(const[o,t]of Object.entries(e.guideConfigs.legend??{})){const s=n.guideConfigs.legend?.[o];if(u&&s!==void 0||!u&&t===s)continue;const p=s??Object.entries(n.guideConfigs.legend??{}).find(([l,g])=>g.target===t.target&&O(l).semanticKind===O(o).semanticKind)?.[1],a=r.offset!==void 0?!1:u?!0:p?.inferredOffset===!0;f=f._withLegendConfig(o,{...t,inferredOffset:a})}for(const[o,t]of Object.entries(e.guideConfigs.legendGroups??{})){if(t.placement.at!==void 0){if(t.inferredOffset!==void 0){const{inferredOffset:a,...l}=t;f=f._withMaterializationConfig(["guides","legendGroups",o],l)}continue}const s=n.guideConfigs.legendGroups?.[o];if(t===s)continue;const p=r.offset!==void 0?!1:s?.placement.at!==void 0?!0:s!==void 0?s.inferredOffset===!0:Object.values(f.guideConfigs.legend??{}).filter(a=>a.target===o).every(a=>a.inferredOffset===!0);f=f._withMaterializationConfig(["guides","legendGroups",o],{...t,inferredOffset:p})}return f}export{K as creationLegendTarget,P as prepareLegendGroup,M as recordLegendOffsetIntent,j as validateLegendGroupOptions};
|
|
@@ -1,516 +1 @@
|
|
|
1
|
-
import { editLegendBackground, resolveLegendBackgroundFromBounds } from "./continuous/common.js";
|
|
2
|
-
import { action } from "../../../core/action.js";
|
|
3
|
-
import { noOptions } from "../../../core/validation.js";
|
|
4
|
-
import {
|
|
5
|
-
resolveConcreteGraphicBounds,
|
|
6
|
-
unionConcreteGraphicBounds
|
|
7
|
-
} from "../../../grammar/schemas/graphicBounds.js";
|
|
8
|
-
import { finiteMidpoint } from "../../../grammar/numeric.js";
|
|
9
|
-
import { resolveGraphicBounds } from "../../../layout/canvas.js";
|
|
10
|
-
import {
|
|
11
|
-
resolveHorizontalLegendLane,
|
|
12
|
-
resolveHorizontalLegendGroup,
|
|
13
|
-
resolveSingleHorizontalLegendPlacement,
|
|
14
|
-
isHorizontalEdgeLegend,
|
|
15
|
-
resolveSideLegendLane
|
|
16
|
-
} from "../../../layout/legendLane.js";
|
|
17
|
-
import { findCanvasGraphic } from
|
|
18
|
-
"../../../materialization/graphicHierarchy.js";
|
|
19
|
-
import { legendResourcePolicy } from
|
|
20
|
-
"../../../materialization/guides/resources.js";
|
|
21
|
-
|
|
22
|
-
const FAMILY_ORDER = Object.freeze({
|
|
23
|
-
series: 0,
|
|
24
|
-
color: 0,
|
|
25
|
-
stroke: 0,
|
|
26
|
-
gradient: 0,
|
|
27
|
-
interval: 0,
|
|
28
|
-
strokeGradient: 0,
|
|
29
|
-
strokeInterval: 0,
|
|
30
|
-
size: 1,
|
|
31
|
-
opacity: 2,
|
|
32
|
-
strokeWidth: 3
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
function categoricalFor(program, target) {
|
|
36
|
-
return ["series", "color", "stroke"]
|
|
37
|
-
.map(kind => program.guideConfigs.legend?.[kind])
|
|
38
|
-
.find(config => config?.target === target);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function legendPosition(program, kind, config) {
|
|
42
|
-
if (kind === "size") {
|
|
43
|
-
return categoricalFor(program, config.target)?.position ?? config.position;
|
|
44
|
-
}
|
|
45
|
-
return config.position;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function requestedOffset(config) {
|
|
49
|
-
return config.offset;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
function existingIds(program, kind) {
|
|
53
|
-
return legendResourcePolicy(kind).graphicIds.filter(
|
|
54
|
-
id => program.graphicSpec.objects[id] !== undefined
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function componentIds(program, kind) {
|
|
59
|
-
const ids = existingIds(program, kind);
|
|
60
|
-
const titleId = ids.find(id => id.endsWith("Title"));
|
|
61
|
-
const labelId = ids.find(id => id.endsWith("Labels"));
|
|
62
|
-
const backgroundId = ids.find(id => id.endsWith("Background"));
|
|
63
|
-
const symbolIds = ids.filter(id =>
|
|
64
|
-
id !== titleId && id !== labelId && id !== backgroundId
|
|
65
|
-
);
|
|
66
|
-
return { titleId, labelId, backgroundId, symbolIds };
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function legendGraphicBounds(program, id) {
|
|
70
|
-
let bounds = resolveConcreteGraphicBounds(program.graphicSpec, id, program.materializationConfigs.textMetrics);
|
|
71
|
-
const graphic = program.graphicSpec.objects[id];
|
|
72
|
-
if (!id.includes("LegendSymbol") || graphic?.type !== "line") return bounds;
|
|
73
|
-
const properties = graphic.items?.map(item => item.properties) ??
|
|
74
|
-
[graphic.properties];
|
|
75
|
-
for (const item of properties) {
|
|
76
|
-
if (
|
|
77
|
-
Object.hasOwn(item, "lineCap") ||
|
|
78
|
-
typeof item.stroke !== "string" ||
|
|
79
|
-
!(item.strokeWidth > 0)
|
|
80
|
-
) continue;
|
|
81
|
-
const extent = item.strokeWidth / 2;
|
|
82
|
-
bounds = {
|
|
83
|
-
left: Math.min(bounds.left, Math.min(item.x1, item.x2) - extent),
|
|
84
|
-
right: Math.max(bounds.right, Math.max(item.x1, item.x2) + extent),
|
|
85
|
-
top: Math.min(bounds.top, Math.min(item.y1, item.y2) - extent),
|
|
86
|
-
bottom: Math.max(bounds.bottom, Math.max(item.y1, item.y2) + extent)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return bounds;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function unionLegendBounds(program, ids) {
|
|
93
|
-
return ids.map(id => legendGraphicBounds(program, id))
|
|
94
|
-
.reduce((result, bounds) => result === undefined
|
|
95
|
-
? bounds
|
|
96
|
-
: {
|
|
97
|
-
left: Math.min(result.left, bounds.left),
|
|
98
|
-
right: Math.max(result.right, bounds.right),
|
|
99
|
-
top: Math.min(result.top, bounds.top),
|
|
100
|
-
bottom: Math.max(result.bottom, bounds.bottom)
|
|
101
|
-
}, undefined);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
function textAnchor(program, id) {
|
|
105
|
-
const graphic = program.graphicSpec.objects[id];
|
|
106
|
-
const properties = graphic.items?.[0]?.properties ?? graphic.properties;
|
|
107
|
-
return properties.x;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function blockDescriptor(program, kind, config) {
|
|
111
|
-
const components = componentIds(program, kind);
|
|
112
|
-
if (components.labelId === undefined || components.symbolIds.length === 0) {
|
|
113
|
-
throw new Error(`Legend lane requires complete ${kind} graphics.`);
|
|
114
|
-
}
|
|
115
|
-
const foregroundIds = [
|
|
116
|
-
...components.symbolIds,
|
|
117
|
-
components.labelId,
|
|
118
|
-
...(components.titleId === undefined ? [] : [components.titleId])
|
|
119
|
-
];
|
|
120
|
-
const bounds = unionLegendBounds(program, foregroundIds);
|
|
121
|
-
const symbolAnchorIds = ["gradient", "strokeGradient"].includes(kind)
|
|
122
|
-
? [kind === "gradient" ? "colorGradientStrips" : "strokeGradientStrips"]
|
|
123
|
-
: components.symbolIds;
|
|
124
|
-
const symbolAnchor = unionLegendBounds(program, symbolAnchorIds);
|
|
125
|
-
const symbolBounds = unionLegendBounds(program, components.symbolIds);
|
|
126
|
-
const labels = resolveConcreteGraphicBounds(
|
|
127
|
-
program.graphicSpec,
|
|
128
|
-
components.labelId
|
|
129
|
-
, program.materializationConfigs.textMetrics);
|
|
130
|
-
const title = components.titleId === undefined
|
|
131
|
-
? undefined
|
|
132
|
-
: (() => {
|
|
133
|
-
const graphic = program.graphicSpec.objects[components.titleId];
|
|
134
|
-
const titleBounds = resolveConcreteGraphicBounds(
|
|
135
|
-
program.graphicSpec,
|
|
136
|
-
components.titleId
|
|
137
|
-
, program.materializationConfigs.textMetrics);
|
|
138
|
-
return {
|
|
139
|
-
x: graphic.properties.x,
|
|
140
|
-
y: graphic.properties.y,
|
|
141
|
-
fontSize: graphic.properties.fontSize,
|
|
142
|
-
width: titleBounds.right - titleBounds.left,
|
|
143
|
-
bounds: titleBounds
|
|
144
|
-
};
|
|
145
|
-
})();
|
|
146
|
-
if ([bounds, symbolAnchor, symbolBounds, labels].includes(undefined)) {
|
|
147
|
-
throw new Error(`Legend lane could not measure ${kind} graphics.`);
|
|
148
|
-
}
|
|
149
|
-
return {
|
|
150
|
-
id: kind,
|
|
151
|
-
kind,
|
|
152
|
-
target: config.target,
|
|
153
|
-
border: borderFor(kind, config),
|
|
154
|
-
offset: requestedOffset(config),
|
|
155
|
-
bounds,
|
|
156
|
-
title,
|
|
157
|
-
symbol: {
|
|
158
|
-
centerX: finiteMidpoint(symbolAnchor.left, symbolAnchor.right),
|
|
159
|
-
left: symbolBounds.left,
|
|
160
|
-
right: symbolBounds.right,
|
|
161
|
-
bounds: symbolAnchor
|
|
162
|
-
},
|
|
163
|
-
labels: {
|
|
164
|
-
x: textAnchor(program, components.labelId),
|
|
165
|
-
width: labels.right - labels.left
|
|
166
|
-
},
|
|
167
|
-
occupiedBounds: unionLegendBounds(program, [
|
|
168
|
-
...foregroundIds,
|
|
169
|
-
...(components.backgroundId === undefined ? [] : [components.backgroundId])
|
|
170
|
-
]),
|
|
171
|
-
...components,
|
|
172
|
-
foregroundIds
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function borderFor(kind, config) {
|
|
177
|
-
return [
|
|
178
|
-
"series", "color", "stroke", "gradient", "strokeGradient", "opacity",
|
|
179
|
-
"interval", "strokeInterval", "strokeWidth", "size"
|
|
180
|
-
].includes(kind)
|
|
181
|
-
? config.border
|
|
182
|
-
: false;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
function groupBlocks(blocks, configs) {
|
|
186
|
-
const groups = [];
|
|
187
|
-
for (let index = 0; index < blocks.length; index += 1) {
|
|
188
|
-
const block = blocks[index];
|
|
189
|
-
const config = configs[block.kind];
|
|
190
|
-
const next = blocks[index + 1];
|
|
191
|
-
if (
|
|
192
|
-
["series", "color", "stroke"].includes(block.kind) &&
|
|
193
|
-
next?.kind === "size" && next.target === block.target
|
|
194
|
-
) {
|
|
195
|
-
groups.push({
|
|
196
|
-
id: `${block.kind}+size`,
|
|
197
|
-
blocks: [block, next.backgroundId === undefined ? next : { ...next,
|
|
198
|
-
bounds: next.occupiedBounds, inset: next.border.padding + next.border.lineWidth / 2 }],
|
|
199
|
-
border: borderFor(block.kind, config),
|
|
200
|
-
backgroundId: block.backgroundId
|
|
201
|
-
});
|
|
202
|
-
index += 1;
|
|
203
|
-
continue;
|
|
204
|
-
}
|
|
205
|
-
groups.push({
|
|
206
|
-
id: block.kind,
|
|
207
|
-
blocks: [block],
|
|
208
|
-
border: borderFor(block.kind, config),
|
|
209
|
-
backgroundId: block.backgroundId
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
return groups;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
function edgeKinds(program, edge) {
|
|
216
|
-
const configs = program.guideConfigs.legend ?? {};
|
|
217
|
-
const layerOrder = new Map(program.semanticSpec.layers.map(
|
|
218
|
-
(layer, index) => [layer.id, index]
|
|
219
|
-
));
|
|
220
|
-
return Object.entries(configs)
|
|
221
|
-
.filter(([kind, config]) => legendPosition(program, kind, config) === edge)
|
|
222
|
-
.sort(([kindA, configA], [kindB, configB]) =>
|
|
223
|
-
(layerOrder.get(configA.target) - layerOrder.get(configB.target)) ||
|
|
224
|
-
(FAMILY_ORDER[kindA] - FAMILY_ORDER[kindB])
|
|
225
|
-
);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
export function hasMultiSideLegendLane(program) {
|
|
229
|
-
return ["right", "left"].some(side => edgeKinds(program, side).length > 1);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export function hasHorizontalLegendLane(program) {
|
|
233
|
-
return ["top", "bottom"].some(edge => edgeKinds(program, edge).some(
|
|
234
|
-
([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
|
|
235
|
-
));
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
export function hasLegendLane(program) {
|
|
239
|
-
return hasMultiSideLegendLane(program) ||
|
|
240
|
-
hasHorizontalLegendLane(program);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
function translateCommands(commands, dx, dy) {
|
|
244
|
-
return commands.map(command => {
|
|
245
|
-
if (command.op === "Z") return { ...command };
|
|
246
|
-
const next = {
|
|
247
|
-
...command,
|
|
248
|
-
x: command.x + dx,
|
|
249
|
-
y: command.y + dy
|
|
250
|
-
};
|
|
251
|
-
if (command.op === "C") {
|
|
252
|
-
next.x1 = command.x1 + dx;
|
|
253
|
-
next.y1 = command.y1 + dy;
|
|
254
|
-
next.x2 = command.x2 + dx;
|
|
255
|
-
next.y2 = command.y2 + dy;
|
|
256
|
-
}
|
|
257
|
-
return next;
|
|
258
|
-
});
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function translatedProperties(type, properties, dx, dy) {
|
|
262
|
-
const next = { ...properties };
|
|
263
|
-
if (["circle", "rect", "text"].includes(type)) {
|
|
264
|
-
next.x += dx;
|
|
265
|
-
next.y += dy;
|
|
266
|
-
} else if (type === "line") {
|
|
267
|
-
next.x1 += dx;
|
|
268
|
-
next.x2 += dx;
|
|
269
|
-
next.y1 += dy;
|
|
270
|
-
next.y2 += dy;
|
|
271
|
-
} else if (type === "path") {
|
|
272
|
-
next.commands = translateCommands(next.commands, dx, dy);
|
|
273
|
-
}
|
|
274
|
-
return next;
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function translateGraphic(program, id, dx, dy) {
|
|
278
|
-
const graphic = program.graphicSpec.objects[id];
|
|
279
|
-
if (dx === 0 && dy === 0) return program;
|
|
280
|
-
if (graphic.items !== undefined) {
|
|
281
|
-
return program.editGraphics({
|
|
282
|
-
target: id,
|
|
283
|
-
property: "items",
|
|
284
|
-
value: graphic.items.map(item => ({
|
|
285
|
-
type: item.type ?? graphic.type,
|
|
286
|
-
properties: translatedProperties(
|
|
287
|
-
item.type ?? graphic.type,
|
|
288
|
-
item.properties,
|
|
289
|
-
dx,
|
|
290
|
-
dy
|
|
291
|
-
)
|
|
292
|
-
}))
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
let next = program;
|
|
296
|
-
const properties = translatedProperties(
|
|
297
|
-
graphic.type,
|
|
298
|
-
graphic.properties,
|
|
299
|
-
dx,
|
|
300
|
-
dy
|
|
301
|
-
);
|
|
302
|
-
for (const [property, value] of Object.entries(properties)) {
|
|
303
|
-
if (value !== graphic.properties[property]) {
|
|
304
|
-
next = next.editGraphics({ target: id, property, value });
|
|
305
|
-
}
|
|
306
|
-
}
|
|
307
|
-
return next;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function horizontalGroups(program, groups) {
|
|
311
|
-
return groups.map(group => {
|
|
312
|
-
const representative = group.blocks[0];
|
|
313
|
-
const config = program.guideConfigs.legend?.[representative.kind];
|
|
314
|
-
const atomic = group.blocks.length > 1;
|
|
315
|
-
const titleId = atomic ? undefined : representative.titleId;
|
|
316
|
-
const contentIds = group.blocks.flatMap(block => block.foregroundIds)
|
|
317
|
-
.filter(id => id !== titleId);
|
|
318
|
-
if (atomic) contentIds.push(...group.blocks.slice(1).flatMap(block =>
|
|
319
|
-
block.backgroundId === undefined ? [] : [block.backgroundId]));
|
|
320
|
-
const content = unionLegendBounds(program, contentIds);
|
|
321
|
-
const foreground = unionLegendBounds(
|
|
322
|
-
program,
|
|
323
|
-
[...contentIds, ...(titleId === undefined ? [] : [titleId])]
|
|
324
|
-
);
|
|
325
|
-
if (content === undefined || foreground === undefined) {
|
|
326
|
-
throw new Error(`Legend lane could not measure ${group.id} content.`);
|
|
327
|
-
}
|
|
328
|
-
const border = group.border;
|
|
329
|
-
const inset = border === false
|
|
330
|
-
? 0
|
|
331
|
-
: border.padding + border.lineWidth / 2;
|
|
332
|
-
return {
|
|
333
|
-
id: group.id,
|
|
334
|
-
atomic,
|
|
335
|
-
titleId,
|
|
336
|
-
contentIds,
|
|
337
|
-
title: atomic ? undefined : representative.title,
|
|
338
|
-
inline: config?.titlePosition === "left",
|
|
339
|
-
element: atomic ? foreground : representative.symbol.bounds,
|
|
340
|
-
content,
|
|
341
|
-
horizontal: { left: foreground.left, right: foreground.right,
|
|
342
|
-
top: foreground.top, bottom: foreground.bottom },
|
|
343
|
-
inset,
|
|
344
|
-
padding: border === false ? 0 : border.padding,
|
|
345
|
-
backgroundId: group.backgroundId
|
|
346
|
-
};
|
|
347
|
-
});
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
function applyHorizontalPlan(program, groups, plan) {
|
|
351
|
-
let next = program;
|
|
352
|
-
const byId = new Map(groups.map(group => [group.id, group]));
|
|
353
|
-
for (const placement of plan.placements) {
|
|
354
|
-
const group = byId.get(placement.id);
|
|
355
|
-
if (group.titleId !== undefined) {
|
|
356
|
-
next = translateGraphic(next, group.titleId, placement.dx, placement.titleDy);
|
|
357
|
-
}
|
|
358
|
-
for (const id of group.contentIds) {
|
|
359
|
-
next = translateGraphic(next, id, placement.dx, placement.contentDy);
|
|
360
|
-
}
|
|
361
|
-
if (placement.background !== undefined) {
|
|
362
|
-
for (const property of ["x", "y", "width", "height"]) {
|
|
363
|
-
next = next.editGraphics({ target: placement.background.id, property,
|
|
364
|
-
value: placement.background[property] });
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
}
|
|
368
|
-
if (plan.background !== undefined) {
|
|
369
|
-
for (const property of ["x", "y", "width", "height"]) {
|
|
370
|
-
next = next.editGraphics({ target: plan.background.id, property,
|
|
371
|
-
value: plan.background[property] });
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
return next;
|
|
375
|
-
}
|
|
376
|
-
|
|
377
|
-
export const rematerializeSideLegendLane = /* @__PURE__ */ action(
|
|
378
|
-
{
|
|
379
|
-
op: "rematerializeSideLegendLane",
|
|
380
|
-
description: "Align and stack every right or left legend block."
|
|
381
|
-
},
|
|
382
|
-
function (args = {}) {
|
|
383
|
-
noOptions(args, "rematerializeSideLegendLane");
|
|
384
|
-
const plot = resolveGraphicBounds(this);
|
|
385
|
-
const canvas = findCanvasGraphic(this)?.properties;
|
|
386
|
-
if (plot === undefined || canvas === undefined) {
|
|
387
|
-
throw new Error("Legend lane requires resolved Canvas and plot bounds.");
|
|
388
|
-
}
|
|
389
|
-
const configs = this.guideConfigs.legend ?? {};
|
|
390
|
-
const plans = ["right", "left"].flatMap(side => {
|
|
391
|
-
const entries = edgeKinds(this, side);
|
|
392
|
-
if (entries.length < 2) return [];
|
|
393
|
-
const blocks = entries.map(([kind, config]) =>
|
|
394
|
-
blockDescriptor(this, kind, config)
|
|
395
|
-
);
|
|
396
|
-
const plan = resolveSideLegendLane({
|
|
397
|
-
side,
|
|
398
|
-
plot,
|
|
399
|
-
canvas,
|
|
400
|
-
groups: groupBlocks(blocks, configs)
|
|
401
|
-
});
|
|
402
|
-
return [{ plan, blocks }];
|
|
403
|
-
});
|
|
404
|
-
let next = this;
|
|
405
|
-
for (const { plan, blocks } of plans) {
|
|
406
|
-
const byId = new Map(blocks.map(block => [block.id, block]));
|
|
407
|
-
for (const placement of plan.placements) {
|
|
408
|
-
const block = byId.get(placement.id);
|
|
409
|
-
if (block.titleId !== undefined) {
|
|
410
|
-
next = translateGraphic(
|
|
411
|
-
next,
|
|
412
|
-
block.titleId,
|
|
413
|
-
placement.titleDx,
|
|
414
|
-
placement.dy
|
|
415
|
-
).editGraphics({
|
|
416
|
-
target: block.titleId,
|
|
417
|
-
property: "textAlign",
|
|
418
|
-
value: "left"
|
|
419
|
-
});
|
|
420
|
-
}
|
|
421
|
-
for (const id of block.symbolIds) {
|
|
422
|
-
next = translateGraphic(
|
|
423
|
-
next,
|
|
424
|
-
id,
|
|
425
|
-
placement.symbolDx,
|
|
426
|
-
placement.dy
|
|
427
|
-
);
|
|
428
|
-
}
|
|
429
|
-
next = translateGraphic(
|
|
430
|
-
next,
|
|
431
|
-
block.labelId,
|
|
432
|
-
placement.labelDx,
|
|
433
|
-
placement.dy
|
|
434
|
-
).editGraphics({
|
|
435
|
-
target: block.labelId,
|
|
436
|
-
property: "textAlign",
|
|
437
|
-
value: "left"
|
|
438
|
-
});
|
|
439
|
-
}
|
|
440
|
-
for (const block of blocks) {
|
|
441
|
-
if (block.backgroundId === undefined || plan.backgrounds.some(item => item.id === block.backgroundId)) continue;
|
|
442
|
-
const bounds = unionLegendBounds(next, block.foregroundIds);
|
|
443
|
-
const background = resolveLegendBackgroundFromBounds([bounds], block.border, canvas, "Legend");
|
|
444
|
-
next = editLegendBackground(next, block.backgroundId, background, block.border);
|
|
445
|
-
}
|
|
446
|
-
for (const background of plan.backgrounds) {
|
|
447
|
-
for (const property of ["x", "y", "width", "height"]) {
|
|
448
|
-
next = next.editGraphics({
|
|
449
|
-
target: background.id,
|
|
450
|
-
property,
|
|
451
|
-
value: background[property]
|
|
452
|
-
});
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
}
|
|
456
|
-
return next;
|
|
457
|
-
}
|
|
458
|
-
);
|
|
459
|
-
|
|
460
|
-
export const rematerializeHorizontalLegendLane = /* @__PURE__ */ action(
|
|
461
|
-
{
|
|
462
|
-
op: "rematerializeHorizontalLegendLane",
|
|
463
|
-
description: "Pack and align every top or bottom legend block."
|
|
464
|
-
},
|
|
465
|
-
function (args = {}) {
|
|
466
|
-
noOptions(args, "rematerializeHorizontalLegendLane");
|
|
467
|
-
const plot = resolveGraphicBounds(this);
|
|
468
|
-
const canvas = findCanvasGraphic(this)?.properties;
|
|
469
|
-
if (plot === undefined || canvas === undefined) {
|
|
470
|
-
throw new Error("Legend lane requires resolved Canvas and plot bounds.");
|
|
471
|
-
}
|
|
472
|
-
const configs = this.guideConfigs.legend ?? {};
|
|
473
|
-
let next = this;
|
|
474
|
-
for (const edge of ["top", "bottom"]) {
|
|
475
|
-
const entries = edgeKinds(next, edge).filter(
|
|
476
|
-
([, config]) => isHorizontalEdgeLegend({ ...config, position: edge })
|
|
477
|
-
);
|
|
478
|
-
if (entries.length === 0) continue;
|
|
479
|
-
if (entries.length === 1) {
|
|
480
|
-
const [kind, config] = entries[0];
|
|
481
|
-
const ids = existingIds(next, kind);
|
|
482
|
-
const bounds = unionConcreteGraphicBounds(next.graphicSpec, ids, next.materializationConfigs.textMetrics);
|
|
483
|
-
const { dx, dy } = resolveSingleHorizontalLegendPlacement({
|
|
484
|
-
plot, canvas, config, bounds
|
|
485
|
-
});
|
|
486
|
-
for (const id of ids) next = translateGraphic(next, id, dx, dy);
|
|
487
|
-
continue;
|
|
488
|
-
}
|
|
489
|
-
let groups = groupBlocks(entries.map(([kind, config]) =>
|
|
490
|
-
blockDescriptor(next, kind, config)), configs);
|
|
491
|
-
for (const group of groups.filter(group => group.blocks.length > 1)) {
|
|
492
|
-
const config = configs[group.blocks[0].kind];
|
|
493
|
-
const children = horizontalGroups(next, group.blocks.map((block, index) => ({
|
|
494
|
-
id: block.id, blocks: [block], border: index === 0 ? false : block.border,
|
|
495
|
-
backgroundId: index === 0 ? undefined : block.backgroundId
|
|
496
|
-
}))).map(child => ({ ...child, element: child.content }));
|
|
497
|
-
const plan = resolveHorizontalLegendGroup({ edge, plot, canvas, groups: children,
|
|
498
|
-
align: config.align, offset: config.offset, border: group.border,
|
|
499
|
-
backgroundId: group.backgroundId });
|
|
500
|
-
next = applyHorizontalPlan(next, children, plan);
|
|
501
|
-
}
|
|
502
|
-
if (groups.length < 2) continue;
|
|
503
|
-
groups = groupBlocks(entries.map(([kind, config]) =>
|
|
504
|
-
blockDescriptor(next, kind, config)), configs);
|
|
505
|
-
const horizontal = horizontalGroups(next, groups);
|
|
506
|
-
const plan = resolveHorizontalLegendLane({
|
|
507
|
-
edge,
|
|
508
|
-
plot,
|
|
509
|
-
canvas,
|
|
510
|
-
groups: horizontal
|
|
511
|
-
});
|
|
512
|
-
next = applyHorizontalPlan(next, horizontal, plan);
|
|
513
|
-
}
|
|
514
|
-
return next;
|
|
515
|
-
}
|
|
516
|
-
);
|
|
1
|
+
import{resolveLegendAxisOffset as k,effectiveLegendOffsetConfig as E}from"../../../materialization/guides/resources.js";import{resolveLegendGroupPlacement as q}from"../../../layout/legendGroup.js";import{legendGroup as F}from"../../../materialization/legendGroups.js";import{editLegendBackground as $,resolveLegendBackgroundFromBounds as R}from"./continuous/common.js";import{action as v}from"../../../core/action.js";import{noOptions as z}from"../../../core/validation.js";import{resolveConcreteGraphicBounds as G,unionConcreteGraphicBounds as O}from"../../../grammar/schemas/graphicBounds.js";import{finiteMidpoint as T}from"../../../grammar/numeric.js";import{resolveGraphicBounds as M}from"../../../layout/canvas.js";import{resolveHorizontalLegendLane as K,resolveHorizontalLegendGroup as X,resolveSingleHorizontalLegendPlacement as j,isHorizontalEdgeLegend as P,resolveSideLegendLane as Y,resolveSingleSideLegendPlacement as Z}from"../../../layout/legendLane.js";import{findCanvasGraphic as S}from"../../../materialization/graphicHierarchy.js";import{legendResourcePolicy as _}from"../../../materialization/guides/resources.js";const x=Object.freeze({series:0,color:0,stroke:0,gradient:0,interval:0,strokeGradient:0,strokeInterval:0,size:1,opacity:2,strokeWidth:3});function J(r,i){return["series","color","stroke"].map(t=>r.guideConfigs.legend?.[t]).find(t=>t?.target===i)}function N(r,i,t){return i==="size"&&t.inheritPosition!==!1?J(r,t.target)?.position??t.position:t.position}function Q(r){return r.offset}function m(r,i){return _(i).graphicIds.filter(t=>r.graphicSpec.objects[t]!==void 0)}function U(r,i){const t=m(r,i),o=t.find(c=>c.endsWith("Title")),e=t.find(c=>c.endsWith("Labels")),n=t.find(c=>c.endsWith("Background")),s=t.filter(c=>c!==o&&c!==e&&c!==n);return{titleId:o,labelId:e,backgroundId:n,symbolIds:s}}function V(r,i){let t=G(r.graphicSpec,i,r.materializationConfigs.textMetrics);const o=r.graphicSpec.objects[i];if(!i.includes("LegendSymbol")||o?.type!=="line")return t;const e=o.items?.map(n=>n.properties)??[o.properties];for(const n of e){if(Object.hasOwn(n,"lineCap")||typeof n.stroke!="string"||!(n.strokeWidth>0))continue;const s=n.strokeWidth/2;t={left:Math.min(t.left,Math.min(n.x1,n.x2)-s),right:Math.max(t.right,Math.max(n.x1,n.x2)+s),top:Math.min(t.top,Math.min(n.y1,n.y2)-s),bottom:Math.max(t.bottom,Math.max(n.y1,n.y2)+s)}}return t}function h(r,i){return i.map(t=>V(r,t)).filter(t=>t!==void 0).reduce((t,o)=>t===void 0?o:{left:Math.min(t.left,o.left),right:Math.max(t.right,o.right),top:Math.min(t.top,o.top),bottom:Math.max(t.bottom,o.bottom)},void 0)}function ee(r,i){const t=r.graphicSpec.objects[i];return(t.items?.[0]?.properties??t.properties).x}function w(r,i,t){const o=U(r,i);if(o.labelId===void 0||o.symbolIds.length===0)throw new Error(`Legend lane requires complete ${i} graphics.`);const e=[...o.symbolIds,o.labelId,...o.titleId===void 0?[]:[o.titleId]],n=h(r,e),s=["gradient","strokeGradient"].includes(i)&&t.representation!=="symbols"?[i==="gradient"?"colorGradientStrips":"strokeGradientStrips"]:o.symbolIds,c=h(r,s),f=h(r,o.symbolIds),p=G(r.graphicSpec,o.labelId,r.materializationConfigs.textMetrics),a=r.graphicSpec.objects[o.labelId].items?.length===0,d=o.titleId===void 0?void 0:(()=>{const l=r.graphicSpec.objects[o.titleId],u=G(r.graphicSpec,o.titleId,r.materializationConfigs.textMetrics);return{x:l.properties.x,y:l.properties.y,fontSize:l.properties.fontSize,width:u.right-u.left,bounds:u}})();if([n,c,f].includes(void 0)||p===void 0&&!a)throw new Error(`Legend lane could not measure ${i} graphics.`);return{id:i,kind:i,preserveGrid:(["series","color","stroke"].includes(i)||t.representation==="symbols")&&(t.columns??1)>1,target:t.target,border:C(i,t),offset:Q(t),bounds:n,title:d,symbol:{centerX:T(c.left,c.right),left:f.left,right:f.right,bounds:c},labels:{x:a?f.right+t.labels.offset:ee(r,o.labelId),width:a?0:p.right-p.left},occupiedBounds:h(r,[...e,...o.backgroundId===void 0?[]:[o.backgroundId]]),...o,foregroundIds:e}}function C(r,i){return["series","color","stroke","gradient","strokeGradient","opacity","interval","strokeInterval","strokeWidth","size"].includes(r)?i.border:!1}function B(r,i){const t=[];for(let o=0;o<r.length;o+=1){const e=r[o],n=i[e.kind],s=r[o+1];if(["series","color","stroke"].includes(e.kind)&&s?.kind==="size"&&s.target===e.target){t.push({id:`${e.kind}+size`,blocks:[e,s.backgroundId===void 0?s:{...s,bounds:s.occupiedBounds,inset:s.border.padding+s.border.lineWidth/2}],border:C(e.kind,n),backgroundId:e.backgroundId}),o+=1;continue}t.push({id:e.kind,blocks:[e],border:C(e.kind,n),backgroundId:e.backgroundId})}return t}function I(r,i){const t=r.guideConfigs.legend??{},o=new Map(r.semanticSpec.layers.map((e,n)=>[e.id,n]));return Object.entries(t).filter(([e,n])=>F(r,n.target)===void 0&&N(r,e,n)===i).sort(([e,n],[s,c])=>o.get(n.target)-o.get(c.target)||x[e]-x[s])}function te(r){return["right","left"].some(i=>I(r,i).length>0)}function ne(r){return["top","bottom"].some(i=>I(r,i).some(([,t])=>P({...t,position:i})))}function be(r){return te(r)||ne(r)}function oe(r,i,t){return r.map(o=>{if(o.op==="Z")return{...o};const e={...o,x:o.x+i,y:o.y+t};return o.op==="C"&&(e.x1=o.x1+i,e.y1=o.y1+t,e.x2=o.x2+i,e.y2=o.y2+t),e})}function H(r,i,t,o){const e={...i};return["circle","rect","text"].includes(r)?(e.x+=t,e.y+=o):r==="line"?(e.x1+=t,e.x2+=t,e.y1+=o,e.y2+=o):r==="path"&&(e.commands=oe(e.commands,t,o)),e}function b(r,i,t,o){const e=r.graphicSpec.objects[i];if(t===0&&o===0)return r;if(e.items!==void 0)return r.editGraphics({target:i,property:"items",value:e.items.map(c=>({type:c.type??e.type,properties:H(c.type??e.type,c.properties,t,o)}))});let n=r;const s=H(e.type,e.properties,t,o);for(const[c,f]of Object.entries(s))f!==e.properties[c]&&(n=n.editGraphics({target:i,property:c,value:f}));return n}function W(r,i){const t=e=>e.textBaseline==="middle"?{...e,y:e.y-e.fontSize/2,textBaseline:"top"}:e;let o=r;for(const e of i){const n=o.graphicSpec.objects[e];if(n.type==="text")if(n.items!==void 0){if(!n.items.some(s=>s.properties.textBaseline==="middle"))continue;o=o.editGraphics({target:e,property:"items",value:n.items.map(s=>({type:s.type??n.type,properties:t(s.properties)}))})}else n.properties.textBaseline==="middle"&&(o=o.editGraphics({target:e,property:"y",value:t(n.properties).y}).editGraphics({target:e,property:"textBaseline",value:"top"}))}return o}function A(r,i){return i.map(t=>{const o=t.blocks[0],e=r.guideConfigs.legend?.[o.kind],n=t.blocks.length>1,s=n?void 0:o.titleId,c=t.blocks.flatMap(l=>l.foregroundIds).filter(l=>l!==s);n&&c.push(...t.blocks.slice(1).flatMap(l=>l.backgroundId===void 0?[]:[l.backgroundId]));const f=h(r,c),p=h(r,[...c,...s===void 0?[]:[s]]);if(f===void 0||p===void 0)throw new Error(`Legend lane could not measure ${t.id} content.`);const a=t.border,d=a===!1?0:a.padding+a.lineWidth/2;return{id:t.id,atomic:n,titleId:s,contentIds:c,title:n?void 0:o.title,inline:e?.titlePosition==="left",element:n?p:o.symbol.bounds,content:f,horizontal:{left:p.left,right:p.right,top:p.top,bottom:p.bottom},inset:d,padding:a===!1?0:a.padding,backgroundId:t.backgroundId}})}function D(r,i,t){let o=r;const e=new Map(i.map(n=>[n.id,n]));for(const n of t.placements){const s=e.get(n.id);s.titleId!==void 0&&(o=b(o,s.titleId,n.dx,n.titleDy));for(const c of s.contentIds)o=b(o,c,n.dx,n.contentDy);if(n.background!==void 0)for(const c of["x","y","width","height"])o=o.editGraphics({target:n.background.id,property:c,value:n.background[c]})}if(t.background!==void 0)for(const n of["x","y","width","height"])o=o.editGraphics({target:t.background.id,property:n,value:t.background[n]});return o}const me=v({op:"rematerializeSideLegendLane",description:"Align and stack every right or left legend block."},function(r={}){z(r,"rematerializeSideLegendLane");const i=M(this),t=S(this)?.properties;if(i===void 0||t===void 0)throw new Error("Legend lane requires resolved Canvas and plot bounds.");const o=this.guideConfigs.legend??{},e=["right","left"].flatMap(s=>{const c=I(this,s);if(c.length===0)return[];if(c.length===1)return[{side:s,entries:c}];const f=c.map(([a,d])=>w(this,a,d));return[{plan:Y({side:s,plot:i,canvas:t,groups:B(f,o)}),blocks:f,side:s,entries:c}]});let n=this;for(const{plan:s,blocks:c,side:f,entries:p}of e){if(s===void 0)continue;const a=new Map(c.map(d=>[d.id,d]));for(const d of s.placements){const l=a.get(d.id);l.titleId!==void 0&&(n=b(n,l.titleId,d.titleDx,d.dy).editGraphics({target:l.titleId,property:"textAlign",value:"left"}));for(const u of l.symbolIds)n=b(n,u,d.symbolDx,d.dy);n=b(n,l.labelId,d.labelDx,d.dy).editGraphics({target:l.labelId,property:"textAlign",value:"left"})}for(const d of c){if(d.backgroundId===void 0||s.backgrounds.some(g=>g.id===d.backgroundId))continue;const l=h(n,d.foregroundIds),u=R([l],d.border,t,"Legend");n=$(n,d.backgroundId,u,d.border)}for(const d of s.backgrounds)for(const l of["x","y","width","height"])n=n.editGraphics({target:d.id,property:l,value:d[l]})}for(const{side:s,entries:c}of e){const f=c.flatMap(([d])=>m(n,d));n=W(n,f);const p=h(n,f),{dy:a}=Z({side:s,plot:i,canvas:t,bounds:p});for(const d of f)n=b(n,d,0,a)}return n}),ye=v({op:"rematerializeHorizontalLegendLane",description:"Pack and align every top or bottom legend block."},function(r={}){z(r,"rematerializeHorizontalLegendLane");const i=M(this),t=S(this)?.properties;if(i===void 0||t===void 0)throw new Error("Legend lane requires resolved Canvas and plot bounds.");const o=this.guideConfigs.legend??{};let e=this;for(const n of["top","bottom"]){const s=I(e,n).filter(([,a])=>P({...a,position:n})).map(([a,d])=>[a,E(e,a,d)]);if(s.length===0)continue;if(s.length===1){const[a,d]=s[0],l=m(e,a),u=O(e.graphicSpec,l,e.materializationConfigs.textMetrics),{dx:g,dy:y}=j({plot:i,canvas:t,config:{...d,offset:k(e,d,i)},bounds:u});for(const L of l)e=b(e,L,g,y);continue}for(const[a,d]of s){if(d.inferredOffset!==!0||k(e,d,i)===d.offset)continue;const l=m(e,a),u=O(e.graphicSpec,l,e.materializationConfigs.textMetrics),{dx:g,dy:y}=j({plot:i,canvas:t,config:{...d,offset:k(e,d,i)},bounds:u});for(const L of l)e=b(e,L,g,y)}let c=B(s.map(([a,d])=>w(e,a,d)),o);for(const a of c.filter(d=>d.blocks.length>1)){const d=o[a.blocks[0].kind],l=A(e,a.blocks.map((g,y)=>({id:g.id,blocks:[g],border:y===0?!1:g.border,backgroundId:y===0?void 0:g.backgroundId}))).map(g=>({...g,element:g.content})),u=X({edge:n,plot:i,canvas:t,groups:l,allowOverflow:e.materializationConfigs.canvas?.plot!==void 0,align:d.align,offset:k(e,d,i),border:a.border,backgroundId:a.backgroundId});e=D(e,l,u)}if(c.length<2)continue;c=B(s.map(([a,d])=>w(e,a,d)),o);const f=A(e,c),p=K({edge:n,plot:i,canvas:t,groups:f,allowOverflow:e.materializationConfigs.canvas?.plot!==void 0});e=D(e,f,p)}return e}),ke=v({op:"rematerializeLegendGroups",description:"Place each opted-in target legend group from occupied bounds."},function(r={}){z(r,"rematerializeLegendGroups");let i=this;const t=M(i),o=S(i)?.properties;for(const[e,n]of Object.entries(i.guideConfigs.legendGroups??{})){const s=Object.entries(i.guideConfigs.legend??{}).filter(([,a])=>a.target===e).sort(([a],[d])=>x[a]-x[d]);i=W(i,s.flatMap(([a])=>m(i,a)));const c=s.map(([a])=>({id:a,ids:m(i,a),bounds:h(i,m(i,a))})),f=n.placement.at!==void 0?n:{...n,placement:{...n.placement,offset:k(i,{...n.placement,inferredOffset:n.inferredOffset},t)}},p=q({blocks:c,group:f,plot:t,canvas:o,allowOverflow:i.materializationConfigs.canvas?.plot!==void 0});for(const a of p){const d=c.find(l=>l.id===a.id);for(const l of d.ids)i=b(i,l,a.dx,a.dy)}}return i});export{ne as hasHorizontalLegendLane,be as hasLegendLane,te as hasSideLegendLane,ye as rematerializeHorizontalLegendLane,ke as rematerializeLegendGroups,me as rematerializeSideLegendLane};
|
|
@@ -81,6 +81,9 @@ export const removeLegend = /* @__PURE__ */ closedAction(
|
|
|
81
81
|
if (revision !== undefined) {
|
|
82
82
|
next = createCategoricalLegendFromConfig(next, revision.config, revision.order);
|
|
83
83
|
}
|
|
84
|
+
if (!Object.values(next.guideConfigs.legend ?? {}).some(config => config.target === target)) {
|
|
85
|
+
next = next._withoutMaterializationConfig(["guides", "legendGroups", target]);
|
|
86
|
+
}
|
|
84
87
|
const retainedKinds = Object.keys(next.guideConfigs.legend ?? {});
|
|
85
88
|
if (args.channels === undefined) {
|
|
86
89
|
return retainedKinds.length === 0
|
|
@@ -133,7 +133,7 @@ export function resolveLegendSampleValues(config, scale, label = "Legend") {
|
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
export function hasExactLegendSamplingForScale(program, scaleId) {
|
|
136
|
-
return ["size", "opacity", "strokeWidth"].some(kind => {
|
|
136
|
+
return ["size", "opacity", "strokeWidth", "gradient"].some(kind => {
|
|
137
137
|
const config = program.guideConfigs.legend?.[kind];
|
|
138
138
|
return config?.scale === scaleId && readLegendSampling(config).mode === "values";
|
|
139
139
|
});
|