Hide and collapse x-Axis when no data available

I would like to create a graph using chart.js where the x-Axis shows only those ticks for whose there is data available. Of course I could do some preprocessing of the data and eliminate those labels and corresponding 0-values in the data. But once the graph is displayed, i would like to hide/show some datasets and i want the graph to collapse eliminating the zero-values.

All datasets visible:

Graph with all four years of data

Two datasets hidden, some months disappeared from x-Axis:

Graph with only two datasets visible

(To get this picture, i cheated the input data in Excel)

I haven’t found any axis property that would achieve this. Ticks can be shown or hidden, but can the space on the axis collapse so there is no gap?
Is there any way to do it with a callback on each hide/show of a dataset?

I need this as a “workaround solution” for my other question here

Angular App reloads immediately when browser tab is out of focus

My blogging application reloads when i switch tab to a new tab. The moment the tab with the application is out of focus, the application reloads and return to the base route.
For example if I am filling out a form on the ‘/myform’ route and i switch tabs to get information for another tab, when i return, the app reloads to ‘/’ route

I have attempeted cache the last active route. This is implemented in app.component.ts

  ngOnInit() {


    this.router.events.subscribe(event => {
      if (event instanceof NavigationEnd) {
        localStorage.setItem('lastRoute', event.urlAfterRedirects);
      }
    });

    // Check if there's a stored route when the app initializes
    const lastRoute = localStorage.getItem('lastRoute');
    if (lastRoute ) {
      // Navigate to the stored route if it's different from the current route
      this.router.navigateByUrl(lastRoute);
    }

    Listen for visibility change events (when the user leaves or returns to the tab)
    document.addEventListener('visibilitychange', () => {
      if (!document.hidden) {
        console.log('VISIBILITY')
        // When the tab becomes visible again, restore the route
        const lastRoute = localStorage.getItem('lastRoute');
        if (lastRoute && this.router.url !== lastRoute) {
          this.router.navigateByUrl(lastRoute);
        }
      }
    });


  
  }

This the route is cached but the reload still happens.

I have also attempted the to use implement the routeReuseStrategy

@Injectable()
export class CustomReuseStrategy implements RouteReuseStrategy {
    shouldReuseRoute(future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot): boolean {
        throw new Error('Method not implemented.');
    }

    store(snapshot: ActivatedRouteSnapshot): DetachedRouteHandle | null {
        // Add logic to determine which routes to store (e.g., by route data)
        if (snapshot.data['store']) {
            return { handle: snapshot.component, selector: snapshot.routeConfig?.path };
        }
        return null;
    }

    shouldAttach(route: ActivatedRouteSnapshot): boolean {
        return !!route.routeConfig && !!route.data['reuse'];
    }

    shouldDetach(route: ActivatedRouteSnapshot): boolean {
        return !!this.store(route); // Detach only if the route is stored
    }

    retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null {
        // Retrieve the previously stored handle based on route information

        const routeConfig = route.routeConfig?.path
        if(routeConfig){
        const stored = localStorage.getItem(routeConfig);
        if (stored) {
            return JSON.parse(stored);
        }
     

        }

        return null;
    }
}

And added it to the list of providers in app.config.ts

export const appConfig: ApplicationConfig = {
  
  providers: [
    provideHttpClient(),
    provideZoneChangeDetection({ eventCoalescing: true }), 
    provideClientHydration(withHttpTransferCacheOptions({
      includePostRequests: true
    })),
    provideRouter(routes), 
    provideStore(reducers, { metaReducers }),
    provideEffects(authEffects,userProfileEffects,blogEffects,linkedinAuthEffects), 
    provideStoreDevtools({
       maxAge: 25, 
       logOnly: !isDevMode(),
       autoPause:true,
       trace:false,
       traceLimit:75 }), 
    provideAnimationsAsync(), 
    { provide: RouteReuseStrategy, useClass: CustomReuseStrategy }

  ]
};

But there is still not change

Textarea scrollHeight Decreases by 2px When Typing After Deleting All Content in Blazor Application

Problem Description

I am experiencing an issue with the autoResize function for a textarea element in my Blazor application. The problem occurs when I have multiple rows of text in the textarea, delete all the content, and then start typing again. The scrollHeight of the textarea decreases by 2px each time I type a character, and it takes around 16 characters before the content height is correct again.

js debug log messages

Code Snippet

Here is the relevant part of my autoResize function in:

export function autoResize(minRows, maxRows) {
    let textarea = document.getElementById("custom-textarea");

    if (textarea == null) {
        return;
    }

    // Ensure consistent box-sizing
    textarea.style.boxSizing = 'border-box';

    const lineHeight = parseFloat(window.getComputedStyle(textarea).lineHeight);
    textarea.style.height = "auto";

    // Calculate the height required based on the content
    const contentHeight = textarea.scrollHeight;
    const divided = contentHeight / lineHeight;
    const lines = Math.floor(divided);
    const rows = Math.min(maxRows, Math.max(minRows, lines));
    const isOverflow = lines > maxRows;

    console.log(`lineHeight: ${lineHeight}, contentHeight: ${contentHeight}, divided: ${divided}, lines: ${lines}, rows: ${rows}, isOverflow: ${isOverflow}`);

    textarea.overflowY = isOverflow ? "scroll" : "hidden";
    textarea.rows = rows;
    textarea.style.height = rows * lineHeight + "px";
    textarea.style.minHeight = lineHeight + "px";
    textarea.style.maxHeight = contentHeight + "px";
    textarea.scrollTop = textarea.scrollHeight;
}

Steps to Reproduce

  1. Type multiple rows of text in the textarea.
  2. Delete all the content.
  3. Start typing again.

Observed Behavior

  • The scrollHeight of the textarea decreases by 2px each time a character is typed.
  • It takes around 16 characters before the content height is correct again.

Expected Behavior

  • The scrollHeight should adjust correctly without decreasing by 2px each time a character is typed.

Additional Information

  • The textarea element is dynamically resized based on its content using the autoResize function.
  • The issue seems to be related to the resetting and recalculating of the textarea height.

Question

How can I fix the autoResize function to ensure that the scrollHeight adjusts correctly without decreasing by 2px each time a character is typed after deleting all content?

What Did I Already Try

  1. Resetting Height to ‘auto’: I ensured that the textarea height is reset to 'auto' before recalculating the new height to allow it to shrink if necessary.

  2. Consistent Box-Sizing: I set the box-sizing property to border-box to include padding and border in the element’s total width and height.

  3. Line Height Calculation: I verified that the line height is being calculated correctly using window.getComputedStyle.

  4. JavaScript Logic: I reviewed the JavaScript logic to ensure that the height is being reset and recalculated correctly.

  5. CSS Styling: I checked the CSS styles to ensure that padding, border, and margin settings are not affecting the height calculations.

  6. Browser Testing: I tested the behavior in different browsers to rule out browser-specific rendering quirks.

Despite these efforts, the scrollHeight of the textarea still decreases by 2px each time a character is typed after deleting all content, and it takes around 16 characters before the content height is correct again.

nextjs promises wont fire to backend but works after restaring browser

So im new to nextjs i have login page setup the problem is after a while if i log out and then do login again, the request is stuck on pending like this picture
promise stuck on pending

but if i restart my browser and do login again it works, and after a few hours this behavior will happen again. So I needed to restart my browser every few minutes/hours
i dont know what is wrong, is it my local machine or the API, or the nextjs itself.
BTW I use PocketBase for authentication.
this is the snippet code for login page

login/page.tsx

export default function Login() {
  const [isLoading, setIsLoading] = useState(false);
  const [isAuthenticating, setIsAuthenticating] = useState(false);
  const form = useForm<LoginFormInputs>({
    resolver: zodResolver(loginSchema),
    defaultValues: {
      email: "",
      password: "",
    },
  });
  const route = useRouter();
  
  const onSubmit: SubmitHandler<LoginFormInputs> = async (data) => {
    setIsLoading(true);
    setIsAuthenticating(true); // Set authenticating state
    try {
      const goLogin = await login(data.email, data.password);
      if(isErrorResponse(goLogin)) {
        if(goLogin.code === 400) {
          toast({title: "Uh Oh!", description: "Invalid login credentials", variant: "destructive"}); 
        }
        if(goLogin.token) {
          toast({title: "Nice!", description: "Login successfully, redirecting.", variant: "success"}); 
          route.push('/dashboard');
        }
      }
    } catch (error) {
      console.error(error);
      return;
    } finally {
      setIsLoading(false);
      setIsAuthenticating(false);
    }
  };

  const handleLoginWithGoogle = async () => {
    setIsAuthenticating(true);
    try {
      await loginWithGoogle();
      toast({title: "Nice!", description: "Login successfully, redirecting.", variant: "success"}); 
      route.push('/dashboard');
    } catch (error) {
      return toast({title: "Nice!", description: "Login successfully, redirecting.", variant: "success"}); 
    } finally {
      setIsAuthenticating(false);
    }
  };
return (<div>....my_code<div/>)

// auth.ts

import pb from "./pocketbase";
import { userlog } from "./userlog";

export const login = async (email: string, password: string) => {
    try {
        pb.authStore.clear();
        const res = await pb.collection('users').authWithPassword(
            email,
            password,
        );
        if(!res.token) {
            return {"code": 400, "message": "Invalid login credentials","data": {}}
        }
        await userlog("login_success");
        return {"code": 200, "message": "Login successfully","token": res.token}
    } catch (error) {
        return {"code": 400, "message": "Invalid login credentials","data": {}}
    }
}

export const loginWithGoogle = async () => {
    try {
        pb.authStore.clear();
        const res = await pb.collection('users').authWithOAuth2({ provider: 'google' });
        await userlog("login_success");
        return res;
    } catch (error) {
        return error;
    }
}

judge value then cause result different in javascript

Do not judge the count’s value, the code works fine, I can read the response from serial port.
enter image description here
If I judge the count’s value first, the count’s value will always be zero, I make true my serial port device works fine.
enter image description here

Can anyone help me? Please!

I make true my serial port device is always ok, the only problem is I can’t judge the readableLength first, if I did, it’s value would always be zero

Applying gray background on HTML

I have a HTML code where I’m trying to apply a gray background on HTML with JavaScript.

The body contains two divs with the wrapper class. On ul with the ID liste1 and liste2 for the second. Each of my ul contains 4 <li> items.

I tried the following script but it did nothing:

  <script>
    //When the page loads, apply the fondgris class to all li elements.
    var liste1 = document.getElementById("liste1");
    for (var i = 0; i < liste1.children.length; i++) {
        liste1.children[i].className = "fondgris";
    }
    
  </script>

Also I called my script in the header of my .html file. Should I add a function to make the code work?

Group data differently in bar chart

I am trying to build a bar-graph using chart.js where i add “random” groups of data. Each group should be labeled with a group name and each bar with the property it really represents.

Data is fetched from a DB and the groups may differ in size, depending on the selection criteria of the user.

In Excel it is possible to achieve a result similar to the desired one (padding each new dataset with 0 for each already existing bar). The drawback is that with each new dataset the bars grow thinner and thinner.

Bar-Graph with grouped bar-categories

A similar result is achievable with Chart.js using the same trick. Each new dataset is padded with null so the bars don’t get mixed up with the previous ones. By setting skipNull: true the bars don’t grow thinner.
So far so good, but when you click on a category name in the legend, its data is hidden, but the labels on X-Axis and the gaps left by the hidden bars still remain. It would be very useful to hide/show some of the added categories to enable a closer comparison between two categories.

In a previous SO-Question, the only answer tells it is not possible to achieve my desired result, but this QA is seven years old, so things might have changed.

Another SO-Post achieves a similar result by reordering the data and putting the Bar-Names as labels on top of the bars, but it loses the ability to hide a category. (The legend is hidden to avoid it, if you show the legend and click on one item, it will not hide a category but the n-th element of all categories. Try it out on the demo of the accepted answer, edit line 76)

An alternative approach would be to add a separate “chart-object” to the same canvas sharing the axes, is there any possibility to achieve this?

How to watch a inner variable of custom question type in SurveyJS?

question

I have defined a customer question type named cascadedropdown, the code is as follows :

ComponentCollection.Instance.add({
  // A unique name; must use lowercase
  name: 'casecadedropdown', // A display name used in the Toolbox
  title: 'casecade dropdown', // A default title for questions created with this question type
  defaultQuestionTitle: 'choose an option',
  elementsJSON: [{
      type: 'dropdown',
      name: 'question1',
      title: 'question1',
      choices: levelOneArray, 
 },{
      type: 'dropdown',
      name: 'question2',
      title: 'question2',
      startWithNewLine: false,
      choices:[],
 },{
      type: 'dropdown',
      name: 'question3',
      title: 'question3',
      startWithNewLine: false,
      choices:[],
      visible: {questionListLen} > 2
 },{
      type: 'dropdown',
      name: 'question4',
      title: 'question4',
      startWithNewLine: false,
      choices:[],
      visible: {questionListLen} > 3
 }
 ],
    calculatedValues:[{
        name:"questionListLen",
        expression: casecadeQuestionList.length
 } ],
  inheritBaseProps: true,
}]

after the cascadedropdown loading, the casecadeQuestionList will be assigned by another value, whether the question3 and question4 display or not depends on the casecadeQuestionList.length, how can I watch the casecadeQuestionList.length change?

trying
I defined the calculated value above, but it does not work. What should I do?

How to horizontally flip webcam Picture-in-Picture video

I am working on a video conferencing application and user can toggle between having their camera to be Picture-in-Picture or just on the web page. For the webpage I used a video element that would use getUserMedia stream as the srcObject and would display the current webcam. However, as the stream from the webcams are mirrored, “the wrong way around”, I used css transform to flip the video element horizontally by 180. It works fine, however, when try to enter in Picture-in-Picture mode, the video is still mirrored / “wrong way around”.

I tried using :picture-in-picture in css to apply the transformation there too, however, it had no effect

const button = document.getElementById('button');
const webCamVideo = document.querySelector('.webcam-vid')

button.addEventListener('click', async () => {
  webCamVideo.srcObject = await navigator.mediaDevices.getUserMedia({ video: true });
  video.play();
  video.addEventListener('loadedmetadata', () => {
    video.requestPictureInPicture()
    .catch(console.error)
  });
});
body {
  padding: 20px;
}

.webcam-vid {
  transform: scaleX(-1)
  width: 100px;
  height: 100px;
  object-fit: cover;
}
<video class="webcam-vid" autoPlay playsInline></video>


<button class="btn btn-primary" id="button">Display Webcam feed in Picture-in-Picture mode</button>

How to combine separate programs into single executable?

I have two separate programs that run in conjunction with each other.

  • project.js which is currently run with just npm run start:project in the directory
    project.py which is currently run with python project.py in the directory.

I need to give this software out to other people but I don’t want them to have direct access to the code or have them manually executing these files in the terminal which is why I think packaging it into a single exe file or two separate exe files to send out would be the best option but I’m not sure how to go about doing so.

Additional project info:

  • The JS file is currently setup to initialize a local endpoint that the python file can access to make certain decisions
  • Would not want/need to host this on the web. Would just be distributed by sending it directly to the user

I’ve looked into posts about turning projects into executables but in all circumstances I could only find them in reference to single projects where all code is written in the same environment.

When using Antd popover, if you enter the setState function when opening, you must click twice for the component to appear

page component

function Page(){
const userList = ...;
const [data, setData] = useState();
return (<>
<UserList users={userList} setData={setData} />
<div>{data}</div>
</>)

user list component

function UserList({ users, setData }) {
return {
users.map((user) => <Item user={user} setData={setData} />
}

item compoent

function Item({ user, setData }) {
const handlePopover = useCallback((newOpen) => {
// I also tried adding something like if (open)
  setData(user);
setOpen(newOpen);
}, [setOpen, setData, user, open]);
return  <Popover
    open={open}
    setOpen={handleOpenChange}
    content={
      <div>{user}</div>
    }
    openButton={<div>click me!</div>}
  />

popover (antd)

import { Popover as AntdPopover, ConfigProvider } from 'antd';
function Popover({ open, setOpen, content, openButton, title = '' }) {
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
};
return (
<ConfigProvider
  theme={{
    token: {
      sizePopupArrow: 0,
    },
  }}
>
  <AntdPopover
    content={content}
    title={title}
    trigger="click"
    open={open}
    onOpenChange={handleOpenChange}
    destroyTooltipOnHide
  >
    {openButton}
  </AntdPopover>
</ConfigProvider>
);
}

I wrote this code in react.js, and the intention was for the data to change when the popover is displayed, but when I setData in this way, I have to press the open button twice. Is there a good way to solve this?

MUI conflict with LangaugeProvider

i’m trying to use MUI React designs for my project, but seems like whenever i try using it I get this error Cannot read properties of null (reading 'useContext'). I ran out of ideas how to fix it, even using MUI throws this error, in my opinion it conflicts with LanguageProvider. All routes in App.js are wrapped around the LanguageProvider. The component code i tried to use MUI looks like this.

import { Link } from 'react-router-dom';
import { AppBar, Toolbar, Button, Typography, Box, Menu, MenuItem } from '@mui/material';
import LanguageContext from '../LanguageContext';
import Login from './login';
import Register from './register';

const Navbar = ({ isLoggedIn, handleLogout, user, setIsLoggedIn, setUser }) => {
 const [showProfile, setShowProfile] = useState(false);
 const [showLogin, setShowLogin] = useState(false);
 const [showRegister, setShowRegister] = useState(false);
 const { language, switchLanguage } = useContext(LanguageContext);

 const toggleProfile = () => setShowProfile(!showProfile);
 
 const openLogin = () => {
   setShowLogin(true);
   setShowRegister(false);
 };
 
 const openRegister = () => {
   setShowRegister(true);
   setShowLogin(false);
 };

 return (
   <AppBar position="static" sx={{ backgroundColor: 'rgba(164, 136, 115, 0.9)', fontFamily: 'Kanit' }}>
     <Toolbar>
       <Typography variant="h6" component={Link} to="/" sx={{ flexGrow: 1, textDecoration: 'none', color: 'inherit' }}>
         eValgykla
       </Typography>
       
       <Box sx={{ display: 'flex', alignItems: 'center' }}>
         {isLoggedIn && !user?.isCashier && (
           <>
             <Button color="inherit" component={Link} to="/cart">{language.Cart}</Button>
             <Button color="inherit" onClick={() => switchLanguage('en')}>
               <img src="https://www.countryflags.com/wp-content/uploads/united-kingdom-flag-png-large.png" alt='English' width={25} height={15} />
             </Button>
             <Button color="inherit" onClick={() => switchLanguage('lt')}>
               <img src="https://cdn.countryflags.com/thumbs/lithuania/flag-400.png" alt='Lithuanian' width={25} height={15} />
             </Button>
             {user && user.isAdmin && (
               <Button color="inherit" component={Link} to="/admin">{language.AdminDashboard}</Button>
             )}
             <Button color="inherit" onClick={toggleProfile}>
               {language.Welcome}, {user?.Name || ''}
             </Button>
             {/* Profile Menu */}
             <Menu open={showProfile} onClose={toggleProfile}>
               <MenuItem component={Link} to="/account">{language.Account}</MenuItem>
               <MenuItem component={Link} to="/payment-history">{language.PaymentHistory}</MenuItem>
               <MenuItem component={Link} to="/your-children">{language.YourChildren}</MenuItem>
               <MenuItem component={Link} to="/help">{language.Help}</MenuItem>
               <MenuItem component={Link} to="/faq">{language.FAQ}</MenuItem>
               <MenuItem onClick={handleLogout}>{language.logout}</MenuItem>
             </Menu>
           </>
         )}

         {isLoggedIn && user?.isCashier && (
           <Button color="inherit" onClick={handleLogout}>{language.logout}</Button>
         )}

         {!isLoggedIn && (
           <>
             <Button color="inherit" onClick={openLogin}>{language.login}</Button>
             <Button color="inherit" onClick={openRegister}>{language.Register}</Button>
             {showLogin && <Login setIsLoggedIn={setIsLoggedIn} setUser={setUser} />}
             {showRegister && <Register onRegisterSuccess={() => { setShowLogin(true); setShowRegister(false); }} />}
           </>
         )}
       </Box>
     </Toolbar>
   </AppBar>
 );
};

export default Navbar;

How to connect to windows server from my node js backend and read the files stored inside windows server

I’m facing an issue with a Node.js/Express.js backend where I need to establish a connection to a Windows server, read files from a specific path, and print the content.

I have the server’s IP address, username, and password, and I’ve verified that the credentials work using Remote Desktop.

What I’ve Tried:

I attempted using ssh2, but it didn’t work.
I also tried using SMB2, but it wasn’t successful either.
My goal is to avoid hosting my backend on the server itself since I need to connect to multiple servers and read files from a specific path via my backend.

Thank you for your help.

Below is the code I’ve written so far:

async function sshConnection(ip, userName, password, privateKeyPath) {
  const conn = new Client();

  return new Promise((resolve, reject) => {
    conn
      .on("ready", () => {
        console.log("SSH Connection Established");

        conn.sftp((err, sftp) => {
          if (err) {
            console.error("SFTP Error:", err);
            conn.end();
            return reject(err);
          }

          sftp.readFile("<file-path>", (err, data) => {
            if (err) {
              console.error("File Read Error:", err);
              conn.end();
              return reject(err);
            }

            console.log("File Data:", data);
            conn.end();
            return resolve(data);
          });
        });
      })
      .on("error", (err) => {
        console.error("SSH Connection Error:", err);
        conn.end();
        return reject(err);
      })
      .on("debug", (info) => {
        console.log("Debug info:", info);
      })
      .on("close", () => {
        console.log("Connection Closed");
      })
      .connect({
        host: ip,
        port: 22,
        username: userName,
        password: password,
      });
  });
}

Using cosine-similarity to find “closeness” between consonants?

I am currently exploring this with ChatGPT, which is basically “how to represent and search/sort rhyming words in English”. To start the problem, I want to find out how similar certain consonant sequences are. Here are the current 29 feature categories I have for consonants so far.

alveolar
approximant
aspiration
bilabial
click
dental
dentalization
explosivity
flap
fricative
glottal
labialization
labiodental
labiovelar
lateral
length
nasal
nasalization
palatal
palatalization
pharyngealization
plosive
retroflex
sibilance
stop
tense
velar
velarization
voiced

Here is how I might map them in JS:

const features = getFeaturesList()

const b = makeData(features, { bilabial: true, voiced: true })
// bh (h is aspiration) in indian languages is common
const bh = makeData(features, { bilabial: true, voiced: true, aspiration: true })
const d = makeData(features, { dental: true, voiced: true })
const dh = makeData(features, { dental: true, voiced: true, aspiration: true })
const p = makeData(features, { bilabial: true })
const ph = makeData(features, { bilabial: true, aspiration: true })
const s = makeData(features, { fricative: true })
const z = makeData(features, { fricative: true, voiced: true })

logData(`b`, b)
logData(`bh`, bh)
logData(`d`, d)
logData(`dh`, dh)
logData(`p`, p)
logData(`ph`, ph)
logData(`s`, s)
logData(`z`, z)

logSimilarity(`b-bh`, b, bh)
logSimilarity(`b-d`, b, d)
logSimilarity(`b-dh`, b, dh)
logSimilarity(`b-p`, b, p)
logSimilarity(`b-ph`, b, ph)
logSimilarity(`b-s`, b, s)
logSimilarity(`b-z`, b, z)

function getFeaturesList() {
  return {
    bilabial: {
      true: [1],
      false: [0],
    },
    labiodental: {
      true: [1],
      false: [0],
    },
    dental: {
      true: [1],
      false: [0],
    },
    alveolar: {
      true: [1],
      false: [0],
    },
    retroflex: {
      true: [1],
      false: [0],
    },
    palatal: {
      true: [1],
      false: [0],
    },
    velar: {
      true: [1],
      false: [0],
    },
    labiovelar: {
      true: [1],
      false: [0],
    },
    glottal: {
      true: [1],
      false: [0],
    },
    nasal: {
      true: [1],
      false: [0],
    },
    fricative: {
      true: [1],
      false: [0],
    },
    approximant: {
      true: [1],
      false: [0],
    },
    flap: { // r
      true: [1],
      false: [0],
    },
    lateral: {
      true: [1],
      false: [0],
    },
    aspiration: {
      true: [1],
      false: [0],
    },
    click: {
      true: [1],
      false: [0],
    },
    dentalization: {
      true: [1],
      false: [0],
    },
    explosivity: {
      in: [1, 0],
      out: [0, 1],
      false: [0, 0],
    },
    plosive: {
      true: [1],
      false: [0],
    },
    labialization: {
      true: [1],
      false: [0],
    },
    nasalization: {
      true: [1],
      false: [0],
    },
    palatalization: {
      true: [1],
      false: [0],
    },
    pharyngealization: {
      true: [1],
      false: [0],
    },
    stop: {
      true: [1],
      false: [0],
    },
    tense: {
      true: [1],
      false: [0],
    },
    velarization: {
      true: [1],
      false: [0],
    },
    voiced: {
      true: [1],
      false: [0],
    },
    sibilance: {
      true: [1],
      false: [0],
    },
    length: {
      true: [1],
      false: [0],
    },
  }
}

function cosineSimilarity(v1, v2) {
  let dotProduct = 0
  let normA = 0
  let normB = 0
  for (let i = 0; i < v1.length; i++) {
    dotProduct += v1[i] * v2[i]
    normA += Math.pow(v1[i], 2)
    normB += Math.pow(v2[i], 2)
  }
  return dotProduct / (Math.sqrt(normA) * Math.sqrt(normB))
}

function makeData(features, mappings) {
  const featureNames = getSortedFeatureNames()
  const vector = new Array()
  const map = {}
  featureNames.forEach(name => {
    if (mappings[name]) {
      if (!features[name][mappings[name]]) {
        throw new Error(`Missing feature ${name} value ${mappings[name]}.`)
      }
    }
    const provided = features[name][mappings[name]]
    const fallback = features[name].false
    const slice = provided ?? fallback
    vector.push(...slice)

    if (provided && provided !== fallback) {
      map[name] = mappings[name]
    }
  })
  return { map, vector }
}

function getSortedFeatureNames() {
  return [
    'alveolar',
    'approximant',
    'aspiration',
    'bilabial',
    'click',
    'dental',
    'dentalization',
    'explosivity',
    'flap',
    'fricative',
    'glottal',
    'labialization',
    'labiodental',
    'labiovelar',
    'lateral',
    'length',
    'nasal',
    'nasalization',
    'palatal',
    'palatalization',
    'pharyngealization',
    'plosive',
    'retroflex',
    'sibilance',
    'stop',
    'tense',
    'velar',
    'velarization',
    'voiced',
  ]
}

function logSimilarity(key, a, b) {
  console.log(key, cosineSimilarity(a.vector, b.vector))
}

function logData(key, data) {
  console.log(key.padEnd(4, ' '), data.vector.join(''))
  console.log(`  ${JSON.stringify(data.map)}`)
}

That prints this for the vectors/maps:

b    000100000000000000000000000001
  {"bilabial":true,"voiced":true}
bh   001100000000000000000000000001
  {"aspiration":true,"bilabial":true,"voiced":true}
d    000001000000000000000000000001
  {"dental":true,"voiced":true}
dh   001001000000000000000000000001
  {"aspiration":true,"dental":true,"voiced":true}
p    000100000000000000000000000000
  {"bilabial":true}
ph   001100000000000000000000000000
  {"aspiration":true,"bilabial":true}
s    000000000010000000000000000000
  {"fricative":true}
z    000000000010000000000000000001
  {"fricative":true,"voiced":true}

And this is b compared to a few other consonant sounds:

b-bh 0.8164965809277259
b-d 0.4999999999999999
b-dh 0.40824829046386296
b-p 0.7071067811865475
b-ph 0.4999999999999999
b-s 0
b-z 0.4999999999999999

Everything looks decent except b-ph should be closer to b-p, and b-s/b-z should be closer together, maybe like 0.1 or something.

What am I supposed to do from here to compare every possible consonant sound with every other? (Assuming I create the corresponding “feature vector” mapping manually, like I did for these few examples…). How do I make it so similar sounds get a higher score? What manual “supervised learning” sort of stuff do I need to do here? Just in comparing the few hundreds or probably less than 2,000 consonant pairs, that’s all I need to do. Do I need to add more features or something? Or what do I do from here, to get higher “accuracy”.

Basically, what are the high-level rough steps involved in making this work?

Accuracy is defined as “similar consonants are close together”. Similar consonants which are close together is somewhat subjective, but perhaps I could do something to map my subjective interpretation into vectors? How can I do that basically? That is the crux of this question.

Note: The long-term goal in going down this road is to create a system for comparing words which have a “rhyme similarity”. Still a ways to go to get there.

Note 2: We don’t necessarily need to use “cosine similarity” if it’s not a good fit, can use any distance function which makes sense.

Note 3: This is my first foray into implementing anything “AI” related, I have a very basic understanding of how vectors / n-dimensional “features spaces” work at this point, but I would like to learn how to implement a more robust AI/NLP solution to this down the road :).

Note 4: ChatGPT suggests I manually fine-tune feature vectors, (between every consonant pair!!!). That would be a huge amount of work it seems, but is that the basic approach?