@dcg-overseas/number-line 0.1.7 → 0.1.8

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/dist/index.cjs CHANGED
@@ -489,6 +489,32 @@ function computeLabelStep(containerWidth, range) {
489
489
  const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
490
490
  return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
491
491
  }
492
+ function computeHorizontalLayout(minVal, maxVal) {
493
+ const gutter = 16;
494
+ const charWidth = 8;
495
+ const leftLabel = String(Math.round(minVal));
496
+ const rightLabel = String(Math.round(maxVal));
497
+ return {
498
+ padLeft: gutter,
499
+ padRight: gutter,
500
+ labelInsetLeft: Math.max(8, Math.ceil(leftLabel.length * charWidth / 2) + 2),
501
+ labelInsetRight: Math.max(12, Math.ceil(rightLabel.length * charWidth / 2) + 4)
502
+ };
503
+ }
504
+ function createValueToX(viewMin, viewMax, viewWidth, layout) {
505
+ const range = Math.max(viewMax - viewMin, 1e-6);
506
+ const usable = Math.max(viewWidth - layout.padLeft - layout.padRight, 1);
507
+ const innerUsable = Math.max(
508
+ usable - layout.labelInsetLeft - layout.labelInsetRight,
509
+ 1
510
+ );
511
+ return (value) => layout.padLeft + layout.labelInsetLeft + (value - viewMin) / range * innerUsable;
512
+ }
513
+ function edgeTextAnchor(value, domainMin, domainMax) {
514
+ if (Math.abs(value - domainMin) < 1e-9) return "start";
515
+ if (Math.abs(value - domainMax) < 1e-9) return "end";
516
+ return "middle";
517
+ }
492
518
  const ARC_COLORS = [
493
519
  "#7c3aed",
494
520
  "#0891b2",
@@ -504,11 +530,11 @@ function gcd(a, b) {
504
530
  }
505
531
  const AxisLayer = React.memo(function AxisLayer2({
506
532
  min,
533
+ max,
507
534
  viewMin,
508
535
  viewMax,
509
536
  axisY,
510
- padLeft,
511
- padRight,
537
+ horizontalLayout,
512
538
  viewWidth,
513
539
  containerWidth,
514
540
  groupSize,
@@ -520,8 +546,9 @@ const AxisLayer = React.memo(function AxisLayer2({
520
546
  }) {
521
547
  const range = viewMax - viewMin;
522
548
  if (range <= 0) return null;
523
- const usable = viewWidth - padLeft - padRight;
524
- const toX = (v) => padLeft + (v - viewMin) / range * usable;
549
+ const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
550
+ const axisStartX = toX(viewMin);
551
+ const axisEndX = toX(viewMax);
525
552
  const step = Math.max(1, Math.round(tickStep));
526
553
  const rawLabelStep = computeLabelStep(containerWidth, range);
527
554
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
@@ -538,9 +565,9 @@ const AxisLayer = React.memo(function AxisLayer2({
538
565
  /* @__PURE__ */ jsxRuntime.jsx(
539
566
  "line",
540
567
  {
541
- x1: padLeft - 4,
568
+ x1: axisStartX - 4,
542
569
  y1: axisY,
543
- x2: viewWidth - padRight,
570
+ x2: axisEndX,
544
571
  y2: axisY,
545
572
  stroke: "#374151",
546
573
  strokeWidth: 2
@@ -577,7 +604,7 @@ const AxisLayer = React.memo(function AxisLayer2({
577
604
  {
578
605
  x,
579
606
  y: axisY + 22,
580
- textAnchor: "middle",
607
+ textAnchor: edgeTextAnchor(v, min, max),
581
608
  fontSize: 11,
582
609
  fill: "#374151",
583
610
  pointerEvents: "none",
@@ -597,8 +624,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
597
624
  groupSize,
598
625
  groupCount,
599
626
  axisY,
600
- padLeft,
601
- padRight,
627
+ horizontalLayout,
602
628
  viewWidth,
603
629
  arcMaxHeight,
604
630
  tool,
@@ -608,10 +634,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
608
634
  arcColors = ARC_COLORS
609
635
  }) {
610
636
  if (groupSize <= 0 || groupCount <= 0) return null;
611
- const range = viewMax - viewMin;
612
- const usable = viewWidth - padLeft - padRight;
613
- const toX = (v) => padLeft + (v - viewMin) / range * usable;
614
- const clipId = React.useId().replace(/:/g, "_") + "-arcs-clip";
637
+ const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
615
638
  const arcs = [];
616
639
  for (let g = 0; g < groupCount; g++) {
617
640
  if (deletedArcIndices.has(g)) continue;
@@ -677,7 +700,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
677
700
  {
678
701
  x: x2,
679
702
  y: textY,
680
- textAnchor: "middle",
703
+ textAnchor: edgeTextAnchor(end, min, max),
681
704
  fontSize: 10,
682
705
  fill: isSelected ? "#1d4ed8" : color,
683
706
  fontWeight: "600",
@@ -709,18 +732,7 @@ const GroupArcsLayer = React.memo(function GroupArcsLayer2({
709
732
  ] }, g)
710
733
  );
711
734
  }
712
- return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-arcs-layer", children: [
713
- /* @__PURE__ */ jsxRuntime.jsx("defs", { children: /* @__PURE__ */ jsxRuntime.jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsxRuntime.jsx(
714
- "rect",
715
- {
716
- x: padLeft,
717
- y: 0,
718
- width: usable,
719
- height: axisY + 1
720
- }
721
- ) }) }),
722
- /* @__PURE__ */ jsxRuntime.jsx("g", { clipPath: `url(#${clipId})`, children: arcs })
723
- ] });
735
+ return /* @__PURE__ */ jsxRuntime.jsx("g", { className: "nl-arcs-layer", children: arcs });
724
736
  });
725
737
  function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
726
738
  return /* @__PURE__ */ jsxRuntime.jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
@@ -788,9 +800,6 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
788
800
  )
789
801
  ] });
790
802
  }
791
- const PAD_LEFT_LEGACY = 30;
792
- const PAD_RIGHT_LEGACY = 40;
793
- const PAD_X = (PAD_LEFT_LEGACY + PAD_RIGHT_LEGACY) / 2;
794
803
  const LABEL_SPACE_BELOW_AXIS = 42;
795
804
  const LABEL_ABOVE_AXIS_SLOP = 32;
796
805
  const ARC_TOP_PADDING = 16;
@@ -829,8 +838,12 @@ function NumberLine({ className }) {
829
838
  } = useNumberLineContext();
830
839
  const w = containerWidth;
831
840
  const h = containerHeight;
841
+ const horizontalLayout = computeHorizontalLayout(min, max);
832
842
  const range = Math.max(viewMax - viewMin, 1e-6);
833
- const usable = Math.max(w - 2 * PAD_X, 1);
843
+ const usable = Math.max(
844
+ w - horizontalLayout.padLeft - horizontalLayout.padRight - horizontalLayout.labelInsetLeft - horizontalLayout.labelInsetRight,
845
+ 1
846
+ );
834
847
  let ryTypical = MIN_ARC_HEIGHT * 0.5;
835
848
  if (groupSize > 0 && groupCount > 0) {
836
849
  const rx = usable * groupSize / range / 2;
@@ -986,7 +999,8 @@ function NumberLine({ className }) {
986
999
  display: "block",
987
1000
  outline: enableZoom && isZoomActive ? "2px solid rgba(59, 130, 246, 0.5)" : "none",
988
1001
  cursor: svgCursor,
989
- touchAction: "none"
1002
+ touchAction: "none",
1003
+ overflow: "visible"
990
1004
  },
991
1005
  onPointerDown: handlePointerDown,
992
1006
  onPointerMove: handlePointerMove,
@@ -1006,8 +1020,7 @@ function NumberLine({ className }) {
1006
1020
  viewMin,
1007
1021
  viewMax,
1008
1022
  axisY,
1009
- padLeft: PAD_X,
1010
- padRight: PAD_X,
1023
+ horizontalLayout,
1011
1024
  viewWidth: w,
1012
1025
  containerWidth: w,
1013
1026
  groupSize,
@@ -1028,8 +1041,7 @@ function NumberLine({ className }) {
1028
1041
  groupSize,
1029
1042
  groupCount,
1030
1043
  axisY,
1031
- padLeft: PAD_X,
1032
- padRight: PAD_X,
1044
+ horizontalLayout,
1033
1045
  viewWidth: w,
1034
1046
  arcMaxHeight,
1035
1047
  tool,
package/dist/index.js CHANGED
@@ -487,6 +487,32 @@ function computeLabelStep(containerWidth, range) {
487
487
  const candidates = [1, 2, 5, 10, 20, 25, 50, 100, 200, 500];
488
488
  return candidates.find((c) => c >= raw) ?? candidates[candidates.length - 1];
489
489
  }
490
+ function computeHorizontalLayout(minVal, maxVal) {
491
+ const gutter = 16;
492
+ const charWidth = 8;
493
+ const leftLabel = String(Math.round(minVal));
494
+ const rightLabel = String(Math.round(maxVal));
495
+ return {
496
+ padLeft: gutter,
497
+ padRight: gutter,
498
+ labelInsetLeft: Math.max(8, Math.ceil(leftLabel.length * charWidth / 2) + 2),
499
+ labelInsetRight: Math.max(12, Math.ceil(rightLabel.length * charWidth / 2) + 4)
500
+ };
501
+ }
502
+ function createValueToX(viewMin, viewMax, viewWidth, layout) {
503
+ const range = Math.max(viewMax - viewMin, 1e-6);
504
+ const usable = Math.max(viewWidth - layout.padLeft - layout.padRight, 1);
505
+ const innerUsable = Math.max(
506
+ usable - layout.labelInsetLeft - layout.labelInsetRight,
507
+ 1
508
+ );
509
+ return (value) => layout.padLeft + layout.labelInsetLeft + (value - viewMin) / range * innerUsable;
510
+ }
511
+ function edgeTextAnchor(value, domainMin, domainMax) {
512
+ if (Math.abs(value - domainMin) < 1e-9) return "start";
513
+ if (Math.abs(value - domainMax) < 1e-9) return "end";
514
+ return "middle";
515
+ }
490
516
  const ARC_COLORS = [
491
517
  "#7c3aed",
492
518
  "#0891b2",
@@ -502,11 +528,11 @@ function gcd(a, b) {
502
528
  }
503
529
  const AxisLayer = React.memo(function AxisLayer2({
504
530
  min,
531
+ max,
505
532
  viewMin,
506
533
  viewMax,
507
534
  axisY,
508
- padLeft,
509
- padRight,
535
+ horizontalLayout,
510
536
  viewWidth,
511
537
  containerWidth,
512
538
  groupSize,
@@ -518,8 +544,9 @@ const AxisLayer = React.memo(function AxisLayer2({
518
544
  }) {
519
545
  const range = viewMax - viewMin;
520
546
  if (range <= 0) return null;
521
- const usable = viewWidth - padLeft - padRight;
522
- const toX = (v) => padLeft + (v - viewMin) / range * usable;
547
+ const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
548
+ const axisStartX = toX(viewMin);
549
+ const axisEndX = toX(viewMax);
523
550
  const step = Math.max(1, Math.round(tickStep));
524
551
  const rawLabelStep = computeLabelStep(containerWidth, range);
525
552
  const labelStep = Math.max(step, Math.ceil(rawLabelStep / step) * step);
@@ -536,9 +563,9 @@ const AxisLayer = React.memo(function AxisLayer2({
536
563
  /* @__PURE__ */ jsx(
537
564
  "line",
538
565
  {
539
- x1: padLeft - 4,
566
+ x1: axisStartX - 4,
540
567
  y1: axisY,
541
- x2: viewWidth - padRight,
568
+ x2: axisEndX,
542
569
  y2: axisY,
543
570
  stroke: "#374151",
544
571
  strokeWidth: 2
@@ -575,7 +602,7 @@ const AxisLayer = React.memo(function AxisLayer2({
575
602
  {
576
603
  x,
577
604
  y: axisY + 22,
578
- textAnchor: "middle",
605
+ textAnchor: edgeTextAnchor(v, min, max),
579
606
  fontSize: 11,
580
607
  fill: "#374151",
581
608
  pointerEvents: "none",
@@ -595,8 +622,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
595
622
  groupSize,
596
623
  groupCount,
597
624
  axisY,
598
- padLeft,
599
- padRight,
625
+ horizontalLayout,
600
626
  viewWidth,
601
627
  arcMaxHeight,
602
628
  tool,
@@ -606,10 +632,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
606
632
  arcColors = ARC_COLORS
607
633
  }) {
608
634
  if (groupSize <= 0 || groupCount <= 0) return null;
609
- const range = viewMax - viewMin;
610
- const usable = viewWidth - padLeft - padRight;
611
- const toX = (v) => padLeft + (v - viewMin) / range * usable;
612
- const clipId = React.useId().replace(/:/g, "_") + "-arcs-clip";
635
+ const toX = createValueToX(viewMin, viewMax, viewWidth, horizontalLayout);
613
636
  const arcs = [];
614
637
  for (let g = 0; g < groupCount; g++) {
615
638
  if (deletedArcIndices.has(g)) continue;
@@ -675,7 +698,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
675
698
  {
676
699
  x: x2,
677
700
  y: textY,
678
- textAnchor: "middle",
701
+ textAnchor: edgeTextAnchor(end, min, max),
679
702
  fontSize: 10,
680
703
  fill: isSelected ? "#1d4ed8" : color,
681
704
  fontWeight: "600",
@@ -707,18 +730,7 @@ const GroupArcsLayer = memo(function GroupArcsLayer2({
707
730
  ] }, g)
708
731
  );
709
732
  }
710
- return /* @__PURE__ */ jsxs("g", { className: "nl-arcs-layer", children: [
711
- /* @__PURE__ */ jsx("defs", { children: /* @__PURE__ */ jsx("clipPath", { id: clipId, children: /* @__PURE__ */ jsx(
712
- "rect",
713
- {
714
- x: padLeft,
715
- y: 0,
716
- width: usable,
717
- height: axisY + 1
718
- }
719
- ) }) }),
720
- /* @__PURE__ */ jsx("g", { clipPath: `url(#${clipId})`, children: arcs })
721
- ] });
733
+ return /* @__PURE__ */ jsx("g", { className: "nl-arcs-layer", children: arcs });
722
734
  });
723
735
  function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick }) {
724
736
  return /* @__PURE__ */ jsxs("g", { className: "nl-drawing-layer", transform: `scale(${width} ${height})`, children: [
@@ -786,9 +798,6 @@ function DrawingLayer({ strokes, liveStroke, tool, width, height, onStrokeClick
786
798
  )
787
799
  ] });
788
800
  }
789
- const PAD_LEFT_LEGACY = 30;
790
- const PAD_RIGHT_LEGACY = 40;
791
- const PAD_X = (PAD_LEFT_LEGACY + PAD_RIGHT_LEGACY) / 2;
792
801
  const LABEL_SPACE_BELOW_AXIS = 42;
793
802
  const LABEL_ABOVE_AXIS_SLOP = 32;
794
803
  const ARC_TOP_PADDING = 16;
@@ -827,8 +836,12 @@ function NumberLine({ className }) {
827
836
  } = useNumberLineContext();
828
837
  const w = containerWidth;
829
838
  const h = containerHeight;
839
+ const horizontalLayout = computeHorizontalLayout(min, max);
830
840
  const range = Math.max(viewMax - viewMin, 1e-6);
831
- const usable = Math.max(w - 2 * PAD_X, 1);
841
+ const usable = Math.max(
842
+ w - horizontalLayout.padLeft - horizontalLayout.padRight - horizontalLayout.labelInsetLeft - horizontalLayout.labelInsetRight,
843
+ 1
844
+ );
832
845
  let ryTypical = MIN_ARC_HEIGHT * 0.5;
833
846
  if (groupSize > 0 && groupCount > 0) {
834
847
  const rx = usable * groupSize / range / 2;
@@ -984,7 +997,8 @@ function NumberLine({ className }) {
984
997
  display: "block",
985
998
  outline: enableZoom && isZoomActive ? "2px solid rgba(59, 130, 246, 0.5)" : "none",
986
999
  cursor: svgCursor,
987
- touchAction: "none"
1000
+ touchAction: "none",
1001
+ overflow: "visible"
988
1002
  },
989
1003
  onPointerDown: handlePointerDown,
990
1004
  onPointerMove: handlePointerMove,
@@ -1004,8 +1018,7 @@ function NumberLine({ className }) {
1004
1018
  viewMin,
1005
1019
  viewMax,
1006
1020
  axisY,
1007
- padLeft: PAD_X,
1008
- padRight: PAD_X,
1021
+ horizontalLayout,
1009
1022
  viewWidth: w,
1010
1023
  containerWidth: w,
1011
1024
  groupSize,
@@ -1026,8 +1039,7 @@ function NumberLine({ className }) {
1026
1039
  groupSize,
1027
1040
  groupCount,
1028
1041
  axisY,
1029
- padLeft: PAD_X,
1030
- padRight: PAD_X,
1042
+ horizontalLayout,
1031
1043
  viewWidth: w,
1032
1044
  arcMaxHeight,
1033
1045
  tool,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dcg-overseas/number-line",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "Interactive number line component with group arcs and freehand drawing",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -23,12 +23,6 @@
23
23
  "dist"
24
24
  ],
25
25
  "sideEffects": false,
26
- "scripts": {
27
- "build": "vite build",
28
- "build:watch": "vite build --watch",
29
- "typecheck": "tsc --noEmit",
30
- "clean": "rm -rf dist *.tsbuildinfo"
31
- },
32
26
  "peerDependencies": {
33
27
  "react": "^18.0.0",
34
28
  "react-dom": "^18.0.0"
@@ -43,5 +37,11 @@
43
37
  "publishConfig": {
44
38
  "access": "public",
45
39
  "registry": "https://registry.npmjs.org/"
40
+ },
41
+ "scripts": {
42
+ "build": "vite build",
43
+ "build:watch": "vite build --watch",
44
+ "typecheck": "tsc --noEmit",
45
+ "clean": "rm -rf dist *.tsbuildinfo"
46
46
  }
47
- }
47
+ }